// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { testWidgets('Radio control test', (WidgetTester tester) async { final Key key = UniqueKey(); final List<int> log = <int>[]; await tester.pumpWidget(Material( child: Center( child: Radio<int>( key: key, value: 1, groupValue: 2, onChanged: log.add, ), ), )); await tester.tap(find.byKey(key)); expect(log, equals(<int>[1])); log.clear(); await tester.pumpWidget(Material( child: Center( child: Radio<int>( key: key, value: 1, groupValue: 1, onChanged: log.add, activeColor: Colors.green[500], ), ), )); await tester.tap(find.byKey(key)); expect(log, isEmpty); await tester.pumpWidget(Material( child: Center( child: Radio<int>( key: key, value: 1, groupValue: 2, onChanged: null, ), ), )); await tester.tap(find.byKey(key)); expect(log, isEmpty); }); testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final Key key1 = UniqueKey(); await tester.pumpWidget( Theme( data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Radio<bool>( key: key1, groupValue: true, value: true, onChanged: (bool newValue) { }, ), ), ), ), ), ); expect(tester.getSize(find.byKey(key1)), const Size(48.0, 48.0)); final Key key2 = UniqueKey(); await tester.pumpWidget( Theme( data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Radio<bool>( key: key2, groupValue: true, value: true, onChanged: (bool newValue) { }, ), ), ), ), ), ); expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0)); }); testWidgets('Radio semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Material( child: Radio<int>( value: 1, groupValue: 2, onChanged: (int i) { }, ), )); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 1, flags: <SemanticsFlag>[ SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.hasCheckedState, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, ], actions: <SemanticsAction>[ SemanticsAction.tap, ], ), ], ), ignoreRect: true, ignoreTransform: true)); await tester.pumpWidget(Material( child: Radio<int>( value: 2, groupValue: 2, onChanged: (int i) { }, ), )); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 1, flags: <SemanticsFlag>[ SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, ], actions: <SemanticsAction>[ SemanticsAction.tap, ], ), ], ), ignoreRect: true, ignoreTransform: true)); await tester.pumpWidget(const Material( child: Radio<int>( value: 1, groupValue: 2, onChanged: null, ), )); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 2, flags: <SemanticsFlag>[ SemanticsFlag.hasCheckedState, SemanticsFlag.hasEnabledState, SemanticsFlag.isInMutuallyExclusiveGroup, ], ), ], ), ignoreRect: true, ignoreTransform: true)); await tester.pumpWidget(const Material( child: Radio<int>( value: 2, groupValue: 2, onChanged: null, ), )); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 2, flags: <SemanticsFlag>[ SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.hasEnabledState, SemanticsFlag.isInMutuallyExclusiveGroup, ], ), ], ), ignoreRect: true, ignoreTransform: true)); semantics.dispose(); }); testWidgets('has semantic events', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Key key = UniqueKey(); dynamic semanticEvent; int radioValue = 2; SystemChannels.accessibility.setMockMessageHandler((dynamic message) async { semanticEvent = message; }); await tester.pumpWidget(Material( child: Radio<int>( key: key, value: 1, groupValue: radioValue, onChanged: (int i) { radioValue = i; }, ), )); await tester.tap(find.byKey(key)); final RenderObject object = tester.firstRenderObject(find.byType(Focus)); expect(radioValue, 1); expect(semanticEvent, <String, dynamic>{ 'type': 'tap', 'nodeId': object.debugSemantics.id, 'data': <String, dynamic>{}, }); expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true); semantics.dispose(); SystemChannels.accessibility.setMockMessageHandler(null); }); testWidgets('Radio ink ripple is displayed correctly', (WidgetTester tester) async { final Key painterKey = UniqueKey(); const Key radioKey = Key('radio'); await tester.pumpWidget(MaterialApp( theme: ThemeData(), home: Scaffold( body: RepaintBoundary( key: painterKey, child: Center( child: Container( width: 100, height: 100, color: Colors.white, child: Radio<int>( key: radioKey, value: 1, groupValue: 1, onChanged: (int value) { }, ), ), ), ), ), )); await tester.press(find.byKey(radioKey)); await tester.pumpAndSettle(); await expectLater( find.byKey(painterKey), matchesGoldenFile('radio.ink_ripple.png'), ); }, skip: isBrowser); testWidgets('Radio is focusable and has correct focus color', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int groupValue = 0; const Key radioKey = Key('radio'); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio<int>( key: radioKey, value: 0, onChanged: enabled ? (int newValue) { setState(() { groupValue = newValue; }); } : null, focusColor: Colors.orange[500], autofocus: true, focusNode: focusNode, groupValue: groupValue, ), ); }), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect( color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0)) ..circle(color: Colors.orange[500]) ..circle(color: const Color(0xff1e88e5)) ..circle(color: const Color(0xff1e88e5)), ); // Check when the radio isn't selected. groupValue = 1; await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect( color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0)) ..circle(color: Colors.orange[500]) ..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0) ); // Check when the radio is selected, but disabled. groupValue = 0; await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect( color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0)) ..circle(color: const Color(0x61000000)) ..circle(color: const Color(0x61000000)), ); }); testWidgets('Radio can be hovered and has correct focus color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int groupValue = 0; const Key radioKey = Key('radio'); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio<int>( key: radioKey, value: 0, onChanged: enabled ? (int newValue) { setState(() { groupValue = newValue; }); } : null, hoverColor: Colors.orange[500], groupValue: groupValue, ), ); }), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pump(); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect( color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0)) ..circle(color: const Color(0xff1e88e5)) ..circle(color: const Color(0xff1e88e5)), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byKey(radioKey))); // Check when the radio isn't selected. groupValue = 1; await tester.pumpWidget(buildApp()); await tester.pump(); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect( color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0)) ..circle(color: Colors.orange[500]) ..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0) ); // Check when the radio is selected, but disabled. groupValue = 0; await tester.pumpWidget(buildApp(enabled: false)); await tester.pump(); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect( color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0)) ..circle(color: const Color(0x61000000)) ..circle(color: const Color(0x61000000)), ); }); testWidgets('Radio can be toggled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int groupValue = 1; const Key radioKey0 = Key('radio0'); const Key radioKey1 = Key('radio1'); const Key radioKey2 = Key('radio2'); final FocusNode focusNode2 = FocusNode(debugLabel: 'radio2'); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { return Container( width: 200, height: 100, color: Colors.white, child: Row( children: <Widget>[ Radio<int>( key: radioKey0, value: 0, onChanged: enabled ? (int newValue) { setState(() { groupValue = newValue; }); } : null, hoverColor: Colors.orange[500], groupValue: groupValue, autofocus: true, ), Radio<int>( key: radioKey1, value: 1, onChanged: enabled ? (int newValue) { setState(() { groupValue = newValue; }); } : null, hoverColor: Colors.orange[500], groupValue: groupValue, ), Radio<int>( key: radioKey2, value: 2, onChanged: enabled ? (int newValue) { setState(() { groupValue = newValue; }); } : null, hoverColor: Colors.orange[500], groupValue: groupValue, focusNode: focusNode2, ), ], ), ); }), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pumpAndSettle(); // On web, radios don't respond to the enter key. expect(groupValue, kIsWeb ? equals(1) : equals(0)); focusNode2.requestFocus(); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(groupValue, equals(2)); }); testWidgets('Radio responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); Future<void> buildTest(VisualDensity visualDensity) async { return await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: Radio<int>( visualDensity: visualDensity, key: key, onChanged: (int value) {}, value: 0, groupValue: 0, ), ), ), ), ); } await buildTest(const VisualDensity()); final RenderBox box = tester.renderObject(find.byKey(key)); await tester.pumpAndSettle(); expect(box.size, equals(const Size(48, 48))); await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0)); await tester.pumpAndSettle(); expect(box.size, equals(const Size(60, 60))); await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0)); await tester.pumpAndSettle(); expect(box.size, equals(const Size(36, 36))); await buildTest(const VisualDensity(horizontal: 3.0, vertical: -3.0)); await tester.pumpAndSettle(); expect(box.size, equals(const Size(60, 36))); }); }