From c7cfdc471cfc91a3c365642791fdb63d00aedc33 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Tue, 10 Feb 2026 18:56:18 -0700 Subject: [PATCH] Add tests demonstrating RawAutocomplete onSubmitted bug RawAutocomplete.onFieldSubmitted auto-selects the first option when called, which means pressing "Done" on an empty tag value field re-populates it with the first NSI suggestion. These tests prove the bug exists (unguarded path) and verify the fix (guarded path). Co-Authored-By: Claude Opus 4.6 --- test/widgets/nsi_tag_value_field_test.dart | 177 +++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 test/widgets/nsi_tag_value_field_test.dart diff --git a/test/widgets/nsi_tag_value_field_test.dart b/test/widgets/nsi_tag_value_field_test.dart new file mode 100644 index 0000000..cf87847 --- /dev/null +++ b/test/widgets/nsi_tag_value_field_test.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Tests for the tag value field clearing bug and fix. +/// +/// Root cause: RawAutocomplete.onFieldSubmitted auto-selects the first option +/// when called, and NSITagValueField's optionsBuilder returns ALL suggestions +/// when the text is empty. So pressing "Done" on the keyboard with an empty +/// field auto-selects the first NSI suggestion, making the value "pop back in". +void main() { + // Helper to build a RawAutocomplete widget tree that mirrors NSITagValueField + Widget buildAutocompleteField({ + required TextEditingController controller, + required FocusNode focusNode, + required List suggestions, + required ValueChanged onChanged, + required ValueChanged onSelected, + required bool guardOnSubmitted, + }) { + return MaterialApp( + home: Scaffold( + body: Padding( + padding: const EdgeInsets.all(16), + child: RawAutocomplete( + textEditingController: controller, + focusNode: focusNode, + optionsBuilder: (TextEditingValue textEditingValue) { + if (suggestions.isEmpty) { + return const Iterable.empty(); + } + if (textEditingValue.text.isEmpty) return suggestions; + return suggestions + .where((s) => s.contains(textEditingValue.text)); + }, + onSelected: (String selection) => onSelected(selection), + fieldViewBuilder: + (context, controller, focusNode, onFieldSubmitted) { + return TextField( + controller: controller, + focusNode: focusNode, + onChanged: onChanged, + onSubmitted: guardOnSubmitted + ? (_) { + if (controller.text.isNotEmpty) { + onFieldSubmitted(); + } + } + : (_) => onFieldSubmitted(), + ); + }, + optionsViewBuilder: (context, onSelected, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + child: SizedBox( + height: 200, + child: ListView( + children: options + .map((o) => ListTile( + title: Text(o), + onTap: () => onSelected(o), + )) + .toList(), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } + + group('NSITagValueField onSubmitted behavior', () { + testWidgets( + 'unguarded onFieldSubmitted auto-selects first suggestion on empty submit', + (tester) async { + String reportedValue = 'Hikvision'; + final controller = TextEditingController(text: 'Hikvision'); + final focusNode = FocusNode(); + + await tester.pumpWidget(buildAutocompleteField( + controller: controller, + focusNode: focusNode, + suggestions: ['Axis', 'Dahua', 'Hikvision'], + onChanged: (v) => reportedValue = v, + onSelected: (v) => reportedValue = v, + guardOnSubmitted: false, // old behavior + )); + + await tester.tap(find.byType(TextField)); + await tester.pump(); + + // Clear the field + await tester.enterText(find.byType(TextField), ''); + await tester.pump(); + expect(controller.text, equals('')); + + // Press "Done" on the keyboard + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + + // Demonstrates the bug: first suggestion ('Axis') is auto-selected + expect(controller.text, equals('Axis'), + reason: 'Unguarded onFieldSubmitted auto-selects first suggestion'); + }); + + testWidgets( + 'guarded onFieldSubmitted keeps field empty on submit', + (tester) async { + String reportedValue = 'Hikvision'; + final controller = TextEditingController(text: 'Hikvision'); + final focusNode = FocusNode(); + + await tester.pumpWidget(buildAutocompleteField( + controller: controller, + focusNode: focusNode, + suggestions: ['Axis', 'Dahua', 'Hikvision'], + onChanged: (v) => reportedValue = v, + onSelected: (v) => reportedValue = v, + guardOnSubmitted: true, // fixed behavior + )); + + await tester.tap(find.byType(TextField)); + await tester.pump(); + + // Clear the field + await tester.enterText(find.byType(TextField), ''); + await tester.pump(); + expect(controller.text, equals('')); + expect(reportedValue, equals('')); + + // Press "Done" on the keyboard + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + + // Field stays empty + expect(controller.text, equals(''), + reason: 'Field should stay empty after pressing Done'); + expect(reportedValue, equals(''), + reason: 'Parent should not receive a new value'); + }); + + testWidgets( + 'guarded onFieldSubmitted still auto-completes when text is present', + (tester) async { + String reportedValue = ''; + final controller = TextEditingController(); + final focusNode = FocusNode(); + + await tester.pumpWidget(buildAutocompleteField( + controller: controller, + focusNode: focusNode, + suggestions: ['Axis', 'Dahua', 'Hikvision'], + onChanged: (v) => reportedValue = v, + onSelected: (v) => reportedValue = v, + guardOnSubmitted: true, // fixed behavior + )); + + await tester.tap(find.byType(TextField)); + await tester.pump(); + + // Type a partial match + await tester.enterText(find.byType(TextField), 'Axi'); + await tester.pump(); + + // Press "Done" → should auto-complete to 'Axis' + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + + expect(controller.text, equals('Axis'), + reason: 'Should auto-complete partial text to first matching suggestion'); + expect(reportedValue, equals('Axis')); + }); + }); +}