You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为使用Flutter日期时间选择器的Widget编写日期时间选择单元测试?

测试EditableDateTime组件的日期+时间选择功能

要测试同时选择日期和时间的功能,核心是在日期选择完成后,操作弹出的时间选择器,再验证最终的日期时间值。以下是具体实现步骤和完整代码:

关键实现逻辑

  1. 触发日期选择流程:点击文本框后,先选择目标日期并确认,此时会自动弹出时间选择器
  2. 操作时间选择器:针对24小时制的时间选择器,直接查找目标小时、分钟的文本按钮并点击,最后确认
  3. 验证结果:检查文本框内容是否更新为修改后的完整日期时间

新增测试用例

在原测试组中加入以下测试用例,实现日期与时间的联合测试:

testWidgets(
  'Selecting date and time together',
  (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: EditableDateTime(
            dateTimeTitle: 'Date time',
            dateTimePickerController: dateTimePickerController,
          ),
        ),
      ),
    );

    // 初始化初始时间
    dateTimePickerController.text = '2022-09-20 12:45';
    TextField textField = tester.widget(find.byKey(const Key('editableDateTimeTextField')));
    expect(textField.controller!.text, '2022-09-20 12:45');

    // 触发选择弹窗
    await tester.tap(find.byKey(const Key('editableDateTimeTextField')));
    await tester.pumpAndSettle();

    // 选择日期:9月14日
    await tester.tap(find.text('14'));
    await tester.tap(find.text('OK'));
    await tester.pumpAndSettle();

    // 选择时间:15点30分(24小时制)
    await tester.tap(find.text('15')); // 选择小时
    await tester.tap(find.text('30')); // 选择分钟
    await tester.tap(find.text('OK'));
    await tester.pumpAndSettle();

    // 验证最终结果
    expect(textField.controller!.text, '2022-09-14 15:30');
  },
);

完整测试代码

将上述用例整合后的完整代码如下:

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

/// EditableDateTime widget test code
Future<void> main() async {
  final Finder previousMonthIcon = find.byWidgetPredicate((Widget w) =>
      w is IconButton && (w.tooltip?.startsWith('Previous month') ?? false));

  TextEditingController dateTimePickerController = TextEditingController();

  group(
    'EditableDateTime widget testing',
    () {
      testWidgets(
        'Setting date day only',
        (tester) async {
          await tester.pumpWidget(
            MaterialApp(
              home: Scaffold(
                body: EditableDateTime(
                  dateTimeTitle: 'Date time',
                  dateTimePickerController: dateTimePickerController,
                ),
              ),
            ),
          );

          dateTimePickerController.text = '2022-09-20 12:45';

          TextField textField =
              tester.widget(find.byKey(const Key('editableDateTimeTextField')));

          expect(textField.controller!.text, '2022-09-20 12:45');

          await tester.tap(find.byKey(const Key('editableDateTimeTextField')));
          await tester.pumpAndSettle();
          await tester.tap(find.text('14')); // set day
          await tester.tap(find.text('OK'));
          await tester.pumpAndSettle();
          await tester.tap(find.text('OK'));

          expect(textField.controller!.text, '2022-09-14 12:45');
        },
      );
      testWidgets(
        'Selecting previous date month and setting day only',
        (tester) async {
          await tester.pumpWidget(
            MaterialApp(
              home: Scaffold(
                body: EditableDateTime(
                  dateTimeTitle: 'Date time',
                  dateTimePickerController: dateTimePickerController,
                ),
              ),
            ),
          );

          dateTimePickerController.text = '2022-09-20 12:45';

          TextField textField =
              tester.widget(find.byKey(const Key('editableDateTimeTextField')));

          expect(textField.controller!.text, '2022-09-20 12:45');

          await tester.tap(find.byKey(const Key('editableDateTimeTextField')));
          await tester.pumpAndSettle();
          await tester.tap(previousMonthIcon);
          await tester.pumpAndSettle(const Duration(seconds: 1));
          await tester.tap(find.text('6')); // set day
          await tester.tap(find.text('OK'));
          await tester.pumpAndSettle();
          await tester.tap(find.text('OK'));

          expect(textField.controller!.text, '2022-08-06 12:45');
        },
      );
      // 新增日期时间联合测试用例
      testWidgets(
        'Selecting date and time together',
        (tester) async {
          await tester.pumpWidget(
            MaterialApp(
              home: Scaffold(
                body: EditableDateTime(
                  dateTimeTitle: 'Date time',
                  dateTimePickerController: dateTimePickerController,
                ),
              ),
            ),
          );

          dateTimePickerController.text = '2022-09-20 12:45';
          TextField textField = tester.widget(find.byKey(const Key('editableDateTimeTextField')));
          expect(textField.controller!.text, '2022-09-20 12:45');

          await tester.tap(find.byKey(const Key('editableDateTimeTextField')));
          await tester.pumpAndSettle();

          // 选择日期
          await tester.tap(find.text('14'));
          await tester.tap(find.text('OK'));
          await tester.pumpAndSettle();

          // 选择时间(24小时制)
          await tester.tap(find.text('15'));
          await tester.tap(find.text('30'));
          await tester.tap(find.text('OK'));
          await tester.pumpAndSettle();

          expect(textField.controller!.text, '2022-09-14 15:30');
        },
      );
    },
  );
}

/// EditableDateTime stateless widget definition
class EditableDateTime extends StatelessWidget {
  EditableDateTime({
    Key? key,
    required this.dateTimeTitle,
    required this.dateTimePickerController,
  }) : super(key: key);

  static final DateFormat englishDateTimeFormat =
      DateFormat("yyyy-MM-dd HH:mm");

  DateTime _selectedDate = DateTime.now();
  TimeOfDay _selectedTime = TimeOfDay.now();
  DateTime _dateTime = DateTime.now();

  final String dateTimeTitle;
  final TextEditingController dateTimePickerController;

  void _updateDateTimePickerValues() {
    _selectedDate = _dateTime;
    _selectedTime = TimeOfDay(hour: _dateTime.hour, minute: _dateTime.minute);
    dateTimePickerController.text = englishDateTimeFormat.format(_dateTime);
  }

  // Select for Date
  Future<DateTime?> _selectDatePickerDate(BuildContext context) async {
    final DateTime? selectedDate = await showDatePicker(
      context: context,
      initialDate: _selectedDate,
      firstDate: DateTime(2020),
      lastDate: DateTime(2100),
    );

    if (selectedDate == null) {
      // User clicked on Cancel button
      return null;
    } else {
      if (selectedDate != _selectedDate) {
        _selectedDate = selectedDate;
      }
    }

    return _selectedDate;
  }

  Future<TimeOfDay?> _selectDatePickerTime(BuildContext context) async {
    final TimeOfDay? selectedTime = await showTimePicker(
      context: context,
      initialTime: _selectedTime,
      builder: (
        BuildContext context,
        Widget? childWidget,
      ) {
        return MediaQuery(
          data: MediaQuery.of(context).copyWith(
            alwaysUse24HourFormat: true,
          ),
          child: childWidget!,
        );
      },
    );

    if (selectedTime == null) {
      // User clicked on Cancel button
      return null;
    } else {
      if (selectedTime != _selectedTime) {
        _selectedTime = selectedTime;
      }
    }

    return _selectedTime;
  }

  Future _selectDatePickerDateTime(BuildContext context) async {
    final DateTime? date = await _selectDatePickerDate(context);

    if (date == null) {
      // User clicked on date picker dialog Cancel button. In
      // this case, the time picker dialog is not displayed and
      // the _dateTime value is not modified.
      return;
    }

    final TimeOfDay? time = await _selectDatePickerTime(context);

    if (time == null) {
      // User clicked on time picker dialog Cancel button. In
      // this case, the _dateTime value is not modified.
      return;
    }

//    setState(() {
    _dateTime = DateTime(
      date.year,
      date.month,
      date.day,
      time.hour,
      time.minute,
    );
    //  });

    dateTimePickerController.text = englishDateTimeFormat.format(_dateTime);
  }

  @override
  Widget build(BuildContext context) {
    // print('_EditableDateTimeState.build()');
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              dateTimeTitle,
              style: TextStyle(
                color: Colors.yellow.shade300,
                fontSize: 18.0,
              ),
            ),
            const SizedBox(
              height: 5.0,
            ),
            SizedBox(
              // Required to fix Row exception
              // layoutConstraints.maxWidth < double.infinity.
              width: 170,
              child: Theme(
                data: Theme.of(context).copyWith(
                  textSelectionTheme: TextSelectionThemeData(
                    selectionColor: Colors.blue.shade900,
                  ),
                ),
                child: TextField(
                  key: const Key('editableDateTimeTextField'),
                  decoration: const InputDecoration.collapsed(hintText: ''),
                  style: const TextStyle(
                      color: Colors.white,
                      fontSize: 18.0,
                      fontWeight: FontWeight.normal),
                  controller: dateTimePickerController,
                  readOnly: true,
                  onTap: () {
                    // initializing the date and time dialogs with the
                    // currently displayed date time value ...
                    DateTime currentDateTime = englishDateTimeFormat
                        .parse(dateTimePickerController.text);
                    _selectedTime =
                        TimeOfDay(hour: currentDateTime.hour, minute: currentDateTime.minute);
                    _selectedDate = currentDateTime;
                    _selectDatePickerDateTime(context);
                  },
                ),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

注意事项

  • 时间选择器采用24小时制,直接通过文本查找对应时间按钮即可(如find.text('08')对应8点)
  • 每次弹窗操作后必须调用await tester.pumpAndSettle(),确保UI完全渲染完成
  • 若需测试特殊时间场景(如分钟为0、跨小时切换),只需修改时间选择的文本值即可

内容的提问来源于stack exchange,提问作者Jean-Pierre Schnyder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 06:25:24