// 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. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(['reduced-test-set']) library; import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/gestures/constants.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; void main() { final theme = ThemeData(); testWidgets('Radio control test', (WidgetTester tester) async { final Key key = UniqueKey(); final log = []; await tester.pumpWidget( Theme( data: theme, child: Material( child: Center( child: Radio(key: key, value: 1, groupValue: 2, onChanged: log.add), ), ), ), ); await tester.tap(find.byKey(key)); expect(log, equals([1])); log.clear(); await tester.pumpWidget( Theme( data: theme, child: Material( child: Center( child: Radio( 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( Theme( data: theme, child: Material( child: Center(child: Radio(key: key, value: 1, groupValue: 2)), ), ), ); await tester.tap(find.byKey(key)); expect(log, isEmpty); }); testWidgets('Radio disabled', (WidgetTester tester) async { final Key key = UniqueKey(); final log = []; await tester.pumpWidget( Theme( data: theme, child: Material( child: Center( child: Radio( key: key, value: 1, groupValue: 2, enabled: false, onChanged: log.add, ), ), ), ), ); await tester.tap(find.byKey(key)); expect(log, equals([])); }); testWidgets('Radio can be toggled when toggleable is set', (WidgetTester tester) async { final Key key = UniqueKey(); final log = []; await tester.pumpWidget( Theme( data: theme, child: Material( child: Center( child: Radio( key: key, value: 1, groupValue: 2, onChanged: log.add, toggleable: true, ), ), ), ), ); await tester.tap(find.byKey(key)); expect(log, equals([1])); log.clear(); await tester.pumpWidget( Theme( data: theme, child: Material( child: Center( child: Radio( key: key, value: 1, groupValue: 1, onChanged: log.add, toggleable: true, ), ), ), ), ); await tester.tap(find.byKey(key)); expect(log, equals([null])); log.clear(); await tester.pumpWidget( Theme( data: theme, child: Material( child: Center( child: Radio(key: key, value: 1, onChanged: log.add, toggleable: true), ), ), ), ); await tester.tap(find.byKey(key)); expect(log, equals([1])); }); testWidgets('Radio size is configurable by ThemeData.materialTapTargetSize', ( WidgetTester tester, ) async { final Key key1 = UniqueKey(); await tester.pumpWidget( Theme( data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded), child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Radio( 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: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Radio( key: key2, groupValue: true, value: true, onChanged: (bool? newValue) {}, ), ), ), ), ), ); expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0)); }); testWidgets('Radio selected semantics - platform adaptive', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( Theme( data: theme, child: Material(child: Radio(value: 1, groupValue: 1, onChanged: (int? i) {})), ), ); final bool isCupertino = defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS; expect( semantics, includesNodeWith( flags: [ SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.hasCheckedState, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isChecked, if (isCupertino) SemanticsFlag.hasSelectedState, if (isCupertino) SemanticsFlag.isSelected, ], actions: [ SemanticsAction.tap, if (defaultTargetPlatform != TargetPlatform.iOS) SemanticsAction.focus, ], ), ); semantics.dispose(); }, variant: TargetPlatformVariant.all()); testWidgets('Radio semantics', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( Theme( data: theme, child: Material(child: Radio(value: 1, groupValue: 2, onChanged: (int? i) {})), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics.rootChild( id: 1, flags: [ SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.hasCheckedState, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, ], actions: [SemanticsAction.tap, SemanticsAction.focus], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); await tester.pumpWidget( Theme( data: theme, child: Material(child: Radio(value: 2, groupValue: 2, onChanged: (int? i) {})), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics.rootChild( id: 1, flags: [ SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, ], actions: [SemanticsAction.tap, SemanticsAction.focus], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); await tester.pumpWidget( Theme( data: theme, child: const Material(child: Radio(value: 1, groupValue: 2)), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics.rootChild( id: 1, flags: [ SemanticsFlag.hasCheckedState, SemanticsFlag.hasEnabledState, SemanticsFlag.isInMutuallyExclusiveGroup, SemanticsFlag.isFocusable, // This flag is delayed by 1 frame. ], actions: [SemanticsAction.focus], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); await tester.pump(); // Now the isFocusable should be gone. expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics.rootChild( id: 1, flags: [ SemanticsFlag.hasCheckedState, SemanticsFlag.hasEnabledState, SemanticsFlag.isInMutuallyExclusiveGroup, ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); await tester.pumpWidget( Theme( data: theme, child: const Material(child: Radio(value: 2, groupValue: 2)), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics.rootChild( id: 1, flags: [ SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.hasEnabledState, SemanticsFlag.isInMutuallyExclusiveGroup, ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); testWidgets('has semantic events', (WidgetTester tester) async { final semantics = SemanticsTester(tester); final Key key = UniqueKey(); dynamic semanticEvent; int? radioValue = 2; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler( SystemChannels.accessibility, (dynamic message) async { semanticEvent = message; }, ); await tester.pumpWidget( Theme( data: theme, child: Material( child: Radio( key: key, value: 1, groupValue: radioValue, onChanged: (int? i) { radioValue = i; }, ), ), ), ); await tester.tap(find.byKey(key)); final RenderObject object = tester.firstRenderObject(find.byKey(key)); expect(radioValue, 1); expect(semanticEvent, { 'type': 'tap', 'nodeId': object.debugSemantics!.id, 'data': {}, }); expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true); semantics.dispose(); tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler( SystemChannels.accessibility, null, ); }); testWidgets('Material2 - Radio ink ripple is displayed correctly', (WidgetTester tester) async { final Key painterKey = UniqueKey(); const radioKey = Key('radio'); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: RepaintBoundary( key: painterKey, child: Center( child: Container( width: 100, height: 100, color: Colors.white, child: Radio( key: radioKey, value: 1, groupValue: 1, onChanged: (int? value) {}, ), ), ), ), ), ), ); await tester.press(find.byKey(radioKey)); await tester.pumpAndSettle(); await expectLater(find.byKey(painterKey), matchesGoldenFile('m2_radio.ink_ripple.png')); }); testWidgets('Material3 - Radio ink ripple is displayed correctly', (WidgetTester tester) async { final Key painterKey = UniqueKey(); const radioKey = Key('radio'); await tester.pumpWidget( MaterialApp( home: Scaffold( body: RepaintBoundary( key: painterKey, child: Center( child: Container( width: 100, height: 100, color: Colors.white, child: Radio( key: radioKey, value: 1, groupValue: 1, onChanged: (int? value) {}, ), ), ), ), ), ), ); await tester.press(find.byKey(radioKey)); await tester.pumpAndSettle(); await expectLater(find.byKey(painterKey), matchesGoldenFile('m3_radio.ink_ripple.png')); }); testWidgets('Radio with splash radius set', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double splashRadius = 30; Widget buildApp() { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( value: 0, onChanged: (int? newValue) {}, focusColor: Colors.orange[500], autofocus: true, groupValue: 0, splashRadius: splashRadius, ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byWidgetPredicate((Widget widget) => widget is Radio))), paints..circle(color: Colors.orange[500], radius: splashRadius), ); }); testWidgets('Material2 - Radio is focusable and has correct focus color', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; const radioKey = Key('radio'); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( 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: Colors.transparent) ..circle(color: const Color(0xff2196f3)) ..circle(color: const Color(0xff2196f3)), ); // 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: Colors.transparent) ..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: Colors.transparent) ..circle(color: const Color(0x61000000)) ..circle(color: const Color(0x61000000)), ); focusNode.dispose(); }); testWidgets('Material3 - Radio is focusable and has correct focus color', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; const radioKey = Key('radio'); final theme = ThemeData(); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( 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: Colors.transparent) ..circle(color: theme.colorScheme.primary) ..circle(color: theme.colorScheme.primary), ); // 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() ..circle(color: Colors.orange[500]) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.onSurface), ); // 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: Colors.transparent) ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)) ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)), ); focusNode.dispose(); }); testWidgets('Material2 - Radio can be hovered and has correct hover color', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; const radioKey = Key('radio'); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( 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: Colors.transparent) ..circle(color: const Color(0xff2196f3)) ..circle(color: const Color(0xff2196f3)), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); 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: Colors.transparent) ..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: Colors.transparent) ..circle(color: const Color(0x61000000)) ..circle(color: const Color(0x61000000)), ); }); testWidgets('Material3 - Radio can be hovered and has correct hover color', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 0; const radioKey = Key('radio'); final theme = ThemeData(); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( 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: Colors.transparent) ..circle(color: theme.colorScheme.primary) ..circle(color: theme.colorScheme.primary), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); 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: Colors.transparent) ..circle(color: theme.colorScheme.onSurface, 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: Colors.transparent) ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)) ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)), ); }); testWidgets('Radio can be controlled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; int? groupValue = 1; const radioKey0 = Key('radio0'); const radioKey1 = Key('radio1'); const radioKey2 = Key('radio2'); final focusNode2 = FocusNode(debugLabel: 'radio2'); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 200, height: 100, color: Colors.white, child: Row( children: [ Radio( key: radioKey0, value: 0, onChanged: enabled ? (int? newValue) { setState(() { groupValue = newValue; }); } : null, hoverColor: Colors.orange[500], groupValue: groupValue, autofocus: true, ), Radio( key: radioKey1, value: 1, onChanged: enabled ? (int? newValue) { setState(() { groupValue = newValue; }); } : null, hoverColor: Colors.orange[500], groupValue: groupValue, ), Radio( 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)); focusNode2.dispose(); }); testWidgets('Radio responds to density changes.', (WidgetTester tester) async { const key = Key('test'); Future buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: Center( child: Radio( visualDensity: visualDensity, key: key, onChanged: (int? value) {}, value: 0, groupValue: 0, ), ), ), ), ); } await buildTest(VisualDensity.standard); 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))); }); testWidgets('Radio changes mouse cursor when hovered', (WidgetTester tester) async { const Key key = ValueKey(1); // Test Radio() constructor await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Align( alignment: Alignment.topLeft, child: Material( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Radio( key: key, mouseCursor: SystemMouseCursors.text, value: 1, onChanged: (int? v) {}, groupValue: 2, ), ), ), ), ), ), ); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.addPointer(location: tester.getCenter(find.byKey(key))); addTearDown(gesture.removePointer); await tester.pump(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text, ); // Test default cursor await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Align( alignment: Alignment.topLeft, child: Material( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Radio(value: 1, onChanged: (int? v) {}, groupValue: 2), ), ), ), ), ), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); // Test default cursor when disabled await tester.pumpWidget( MaterialApp( theme: theme, home: const Scaffold( body: Align( alignment: Alignment.topLeft, child: Material( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Radio(value: 1, groupValue: 2), ), ), ), ), ), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); }); testWidgets('Radio button fill color resolves in enabled/disabled states', ( WidgetTester tester, ) async { const activeEnabledFillColor = Color(0xFF000001); const activeDisabledFillColor = Color(0xFF000002); const inactiveEnabledFillColor = Color(0xFF000003); const inactiveDisabledFillColor = Color(0xFF000004); Color getFillColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledFillColor; } return inactiveDisabledFillColor; } if (states.contains(WidgetState.selected)) { return activeEnabledFillColor; } return inactiveEnabledFillColor; } final WidgetStateProperty fillColor = WidgetStateColor.resolveWith(getFillColor); int? groupValue = 0; const radioKey = Key('radio'); Widget buildApp({required bool enabled}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( key: radioKey, value: 0, fillColor: fillColor, onChanged: enabled ? (int? newValue) { setState(() { groupValue = newValue; }); } : null, groupValue: groupValue, ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp(enabled: true)); // Selected and enabled. 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.transparent) ..circle(color: activeEnabledFillColor) ..circle(color: activeEnabledFillColor), ); // Check when the radio isn't selected. groupValue = 1; await tester.pumpWidget(buildApp(enabled: true)); 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.transparent) ..circle(color: inactiveEnabledFillColor, 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( 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.transparent) ..circle(color: activeDisabledFillColor) ..circle(color: activeDisabledFillColor), ); // Check when the radio is unselected and disabled. groupValue = 1; await tester.pumpWidget(buildApp(enabled: false)); 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.transparent) ..circle(color: inactiveDisabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0), ); }); testWidgets('Material2 - Radio fill color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredFillColor = Color(0xFF000001); const focusedFillColor = Color(0xFF000002); Color getFillColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredFillColor; } if (states.contains(WidgetState.focused)) { return focusedFillColor; } return Colors.transparent; } final WidgetStateProperty fillColor = WidgetStateColor.resolveWith(getFillColor); int? groupValue = 0; const radioKey = Key('radio'); final theme = ThemeData(useMaterial3: false); Widget buildApp() { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( autofocus: true, focusNode: focusNode, key: radioKey, value: 0, fillColor: fillColor, onChanged: (int? newValue) { setState(() { groupValue = newValue; }); }, 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.black12) ..circle(color: Colors.transparent) ..circle(color: focusedFillColor), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byKey(radioKey))); 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: theme.hoverColor) ..circle(color: Colors.transparent) ..circle(color: hoveredFillColor), ); focusNode.dispose(); }); testWidgets('Material3 - Radio fill color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredFillColor = Color(0xFF000001); const focusedFillColor = Color(0xFF000002); Color getFillColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredFillColor; } if (states.contains(WidgetState.focused)) { return focusedFillColor; } return Colors.transparent; } final WidgetStateProperty fillColor = WidgetStateColor.resolveWith(getFillColor); int? groupValue = 0; const radioKey = Key('radio'); final theme = ThemeData(); Widget buildApp() { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: Radio( autofocus: true, focusNode: focusNode, key: radioKey, value: 0, fillColor: fillColor, onChanged: (int? newValue) { setState(() { groupValue = newValue; }); }, groupValue: groupValue, ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect() ..circle(color: theme.colorScheme.primary.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: focusedFillColor), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byKey(radioKey))); 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: theme.colorScheme.primary.withOpacity(0.08)) ..circle(color: Colors.transparent) ..circle(color: hoveredFillColor), ); focusNode.dispose(); }); testWidgets('Radio overlay color resolves in active/pressed/focused/hovered states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const fillColor = Color(0xFF000000); const activePressedOverlayColor = Color(0xFF000001); const inactivePressedOverlayColor = Color(0xFF000002); const hoverOverlayColor = Color(0xFF000003); const focusOverlayColor = Color(0xFF000004); const hoverColor = Color(0xFF000005); const focusColor = Color(0xFF000006); Color? getOverlayColor(Set states) { if (states.contains(WidgetState.pressed)) { if (states.contains(WidgetState.selected)) { return activePressedOverlayColor; } return inactivePressedOverlayColor; } if (states.contains(WidgetState.hovered)) { return hoverOverlayColor; } if (states.contains(WidgetState.focused)) { return focusOverlayColor; } return null; } const splashRadius = 24.0; Finder findRadio() { return find.byWidgetPredicate((Widget widget) => widget is Radio); } MaterialInkController? getRadioMaterial(WidgetTester tester) { return Material.of(tester.element(findRadio())); } Widget buildRadio({bool active = false, bool focused = false, bool useOverlay = true}) { return MaterialApp( theme: theme, home: Scaffold( body: Radio( focusNode: focusNode, autofocus: focused, value: active, groupValue: true, onChanged: (_) {}, fillColor: const MaterialStatePropertyAll(fillColor), overlayColor: useOverlay ? WidgetStateProperty.resolveWith(getOverlayColor) : null, hoverColor: hoverColor, focusColor: focusColor, splashRadius: splashRadius, ), ), ); } await tester.pumpWidget(buildRadio(useOverlay: false)); await tester.press(findRadio()); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: splashRadius), reason: 'Default inactive pressed Radio should have overlay color from fillColor', ); await tester.pumpWidget(buildRadio(active: true, useOverlay: false)); await tester.press(findRadio()); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints..circle(color: fillColor.withAlpha(kRadialReactionAlpha), radius: splashRadius), reason: 'Default active pressed Radio should have overlay color from fillColor', ); await tester.pumpWidget(buildRadio()); await tester.press(findRadio()); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints..circle(color: inactivePressedOverlayColor, radius: splashRadius), reason: 'Inactive pressed Radio should have overlay color: $inactivePressedOverlayColor', ); await tester.pumpWidget(buildRadio(active: true)); await tester.press(findRadio()); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints..circle(color: activePressedOverlayColor, radius: splashRadius), reason: 'Active pressed Radio should have overlay color: $activePressedOverlayColor', ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildRadio(focused: true)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( getRadioMaterial(tester), paints..circle(color: focusOverlayColor, radius: splashRadius), reason: 'Focused Radio should use overlay color $focusOverlayColor over $focusColor', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(findRadio())); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints..circle(color: hoverOverlayColor, radius: splashRadius), reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor', ); focusNode.dispose(); }); testWidgets('Do not crash when widget disappears while pointer is down', ( WidgetTester tester, ) async { final Key key = UniqueKey(); Widget buildRadio(bool show) { return MaterialApp( theme: theme, home: Material( child: Center( child: show ? Radio(key: key, value: true, groupValue: false, onChanged: (_) {}) : Container(), ), ), ); } await tester.pumpWidget(buildRadio(true)); final Offset center = tester.getCenter(find.byKey(key)); // Put a pointer down on the screen. final TestGesture gesture = await tester.startGesture(center); await tester.pump(); // While the pointer is down, the widget disappears. await tester.pumpWidget(buildRadio(false)); expect(find.byKey(key), findsNothing); // Release pointer after widget disappeared. await gesture.up(); }); testWidgets('disabled radio shows tooltip', (WidgetTester tester) async { const longPressTooltip = 'long press tooltip'; const tapTooltip = 'tap tooltip'; await tester.pumpWidget( MaterialApp( theme: theme, home: const Material( child: Tooltip( message: longPressTooltip, child: Radio(value: true, groupValue: false), ), ), ), ); // Default tooltip shows up after long pressed. final Finder tooltip0 = find.byType(Tooltip); expect(find.text(longPressTooltip), findsNothing); await tester.tap(tooltip0); await tester.pump(const Duration(milliseconds: 10)); expect(find.text(longPressTooltip), findsNothing); final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip0)); await tester.pump(); await tester.pump(kLongPressTimeout); await gestureLongPress.up(); await tester.pump(); expect(find.text(longPressTooltip), findsOneWidget); // Tooltip shows up after tapping when set triggerMode to TooltipTriggerMode.tap. await tester.pumpWidget( MaterialApp( theme: theme, home: const Material( child: Tooltip( triggerMode: TooltipTriggerMode.tap, message: tapTooltip, child: Radio(value: true, groupValue: false), ), ), ), ); await tester.pump(const Duration(days: 1)); await tester.pumpAndSettle(); expect(find.text(tapTooltip), findsNothing); expect(find.text(longPressTooltip), findsNothing); final Finder tooltip1 = find.byType(Tooltip); await tester.tap(tooltip1); await tester.pump(const Duration(milliseconds: 10)); expect(find.text(tapTooltip), findsOneWidget); }); testWidgets('Material2 - Radio button default colors', (WidgetTester tester) async { Widget buildRadio({bool enabled = true, bool selected = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: Radio(value: true, groupValue: true, onChanged: enabled ? (_) {} : null), ), ); } await tester.pumpWidget(buildRadio()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: Colors.transparent) ..circle(color: const Color(0xFF2196F3)) // Outer circle - primary value ..circle(color: const Color(0xFF2196F3)) ..restore(), // Inner circle - primary value ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildRadio(selected: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..save() ..circle(color: Colors.transparent) ..circle(color: const Color(0xFF2196F3)) ..restore(), ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildRadio(enabled: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: Colors.transparent) ..circle(color: Colors.black38), ); }); testWidgets('Material3 - Radio button default colors', (WidgetTester tester) async { final theme = ThemeData(); Widget buildRadio({bool enabled = true, bool selected = true}) { return MaterialApp( theme: theme, home: Scaffold( body: Radio(value: true, groupValue: true, onChanged: enabled ? (_) {} : null), ), ); } await tester.pumpWidget(buildRadio()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary) // Outer circle - primary value ..circle(color: theme.colorScheme.primary) ..restore(), // Inner circle - primary value ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildRadio(selected: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..save() ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary) ..restore(), ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildRadio(enabled: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38)), ); }); testWidgets('Material2 - Radio button default overlay colors in hover/focus/press states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(useMaterial3: false); final ColorScheme colors = theme.colorScheme; Widget buildRadio({bool enabled = true, bool focused = false, bool selected = true}) { return MaterialApp( theme: theme, home: Scaffold( body: Radio( focusNode: focusNode, autofocus: focused, value: true, groupValue: selected, onChanged: enabled ? (_) {} : null, ), ), ); } // default selected radio await tester.pumpWidget(buildRadio()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: Colors.transparent) ..circle(color: colors.secondary), ); // selected radio in pressed state await tester.pumpWidget(buildRadio()); final TestGesture gesture1 = await tester.startGesture( tester.getCenter(find.byType(Radio)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: colors.secondary.withAlpha(0x1F)) ..circle(color: Colors.transparent) ..circle(color: colors.secondary), ); // unselected radio in pressed state await tester.pumpWidget(buildRadio(selected: false)); final TestGesture gesture2 = await tester.startGesture( tester.getCenter(find.byType(Radio)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: theme.unselectedWidgetColor.withAlpha(0x1F)) ..circle(color: Colors.transparent) ..circle(color: theme.unselectedWidgetColor), ); // selected radio in focused state await tester.pumpWidget(Container()); // reset test await tester.pumpWidget(buildRadio(focused: true)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: theme.focusColor) ..circle(color: Colors.transparent) ..circle(color: colors.secondary), ); // unselected radio in focused state await tester.pumpWidget(Container()); // reset test await tester.pumpWidget(buildRadio(focused: true, selected: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: theme.focusColor) ..circle(color: Colors.transparent) ..circle(color: theme.unselectedWidgetColor), ); // selected radio in hovered state await tester.pumpWidget(Container()); // reset test await tester.pumpWidget(buildRadio()); final TestGesture gesture3 = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture3.addPointer(); await gesture3.moveTo(tester.getCenter(find.byType(Radio))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: theme.hoverColor) ..circle(color: Colors.transparent) ..circle(color: colors.secondary), ); focusNode.dispose(); // Finish gesture to release resources. await gesture1.up(); await gesture2.up(); await tester.pumpAndSettle(); }); testWidgets('Material3 - Radio button default overlay colors in hover/focus/press states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(); final ColorScheme colors = theme.colorScheme; Widget buildRadio({bool enabled = true, bool focused = false, bool selected = true}) { return MaterialApp( theme: theme, home: Scaffold( body: Radio( focusNode: focusNode, autofocus: focused, value: true, groupValue: selected, onChanged: enabled ? (_) {} : null, ), ), ); } // default selected radio await tester.pumpWidget(buildRadio()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: Colors.transparent) ..circle(color: colors.primary.withOpacity(1)), ); // selected radio in pressed state await tester.pumpWidget(buildRadio()); final TestGesture gesture1 = await tester.startGesture( tester.getCenter(find.byType(Radio)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: colors.onSurface.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: colors.primary.withOpacity(1)), ); // unselected radio in pressed state await tester.pumpWidget(buildRadio(selected: false)); final TestGesture gesture2 = await tester.startGesture( tester.getCenter(find.byType(Radio)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: colors.primary.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: colors.onSurfaceVariant.withOpacity(1)), ); // selected radio in focused state await tester.pumpWidget(Container()); // reset test await tester.pumpWidget(buildRadio(focused: true)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: colors.primary.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: colors.primary.withOpacity(1)), ); // unselected radio in focused state await tester.pumpWidget(Container()); // reset test await tester.pumpWidget(buildRadio(focused: true, selected: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: colors.onSurface.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: colors.onSurface.withOpacity(1)), ); // selected radio in hovered state await tester.pumpWidget(Container()); // reset test await tester.pumpWidget(buildRadio()); final TestGesture gesture3 = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture3.addPointer(); await gesture3.moveTo(tester.getCenter(find.byType(Radio))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Radio))), paints ..circle(color: colors.primary.withOpacity(0.08)) ..circle(color: Colors.transparent) ..circle(color: colors.primary.withOpacity(1)), ); focusNode.dispose(); // Finish gesture to release resources. await gesture1.up(); await gesture2.up(); await tester.pumpAndSettle(); }); testWidgets('Radio.adaptive shows the correct platform widget', (WidgetTester tester) async { Widget buildApp(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), home: Material( child: Center(child: Radio.adaptive(value: 1, groupValue: 2, onChanged: (_) {})), ), ); } for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { await tester.pumpWidget(buildApp(platform)); await tester.pumpAndSettle(); expect(find.byType(CupertinoRadio), findsOneWidget); } for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { await tester.pumpWidget(buildApp(platform)); await tester.pumpAndSettle(); expect(find.byType(CupertinoRadio), findsNothing); } }); testWidgets('Material2 - Radio default overlayColor and fillColor resolves pressed state', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(useMaterial3: false); Finder findRadio() { return find.byWidgetPredicate((Widget widget) => widget is Radio); } MaterialInkController? getRadioMaterial(WidgetTester tester) { return Material.of(tester.element(findRadio())); } await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Radio(focusNode: focusNode, value: true, groupValue: true, onChanged: (_) {}), ), ), ); // Hover final Offset center = tester.getCenter(find.byType(Radio)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(center); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints ..circle(color: theme.hoverColor) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.secondary), ); // Highlighted (pressed). await gesture.down(center); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints ..circle(color: theme.colorScheme.secondary.withAlpha(kRadialReactionAlpha)) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.secondary), ); // Remove pressed and hovered states await gesture.up(); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(0, 50)); await tester.pumpAndSettle(); // Focused. focusNode.requestFocus(); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints ..circle(color: theme.focusColor) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.secondary), ); focusNode.dispose(); }); testWidgets('Material3 - Radio default overlayColor and fillColor resolves pressed state', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(); Finder findRadio() { return find.byWidgetPredicate((Widget widget) => widget is Radio); } MaterialInkController? getRadioMaterial(WidgetTester tester) { return Material.of(tester.element(findRadio())); } await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Radio(focusNode: focusNode, value: true, groupValue: true, onChanged: (_) {}), ), ), ); // Hover final Offset center = tester.getCenter(find.byType(Radio)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(center); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints ..circle(color: theme.colorScheme.primary.withOpacity(0.08)) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary), ); // Highlighted (pressed). await gesture.down(center); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints ..circle(color: theme.colorScheme.onSurface.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary), ); // Remove pressed and hovered states await gesture.up(); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(0, 50)); await tester.pumpAndSettle(); // Focused. focusNode.requestFocus(); await tester.pumpAndSettle(); expect( getRadioMaterial(tester), paints ..circle(color: theme.colorScheme.primary.withOpacity(0.1)) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary), ); focusNode.dispose(); }); testWidgets('Radio button background color resolves in enabled/disabled states', ( WidgetTester tester, ) async { const activeEnabledBackgroundColor = Color(0xFF000001); const activeDisabledBackgroundColor = Color(0xFF000002); const inactiveEnabledBackgroundColor = Color(0xFF000003); const inactiveDisabledBackgroundColor = Color(0xFF000004); Color getBackgroundColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledBackgroundColor; } return inactiveDisabledBackgroundColor; } if (states.contains(WidgetState.selected)) { return activeEnabledBackgroundColor; } return inactiveEnabledBackgroundColor; } final WidgetStateProperty backgroundColor = WidgetStateColor.resolveWith( getBackgroundColor, ); int? groupValue = 0; const radioKey = Key('radio'); Widget buildApp({required bool enabled}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: RadioGroup( groupValue: groupValue, onChanged: (int? newValue) { setState(() { groupValue = newValue; }); }, child: Radio( key: radioKey, value: 0, backgroundColor: backgroundColor, enabled: enabled, ), ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp(enabled: true)); // Selected and enabled. 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: activeEnabledBackgroundColor), ); // Check when the radio isn't selected. groupValue = 1; await tester.pumpWidget(buildApp(enabled: true)); 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: inactiveEnabledBackgroundColor), ); // Check when the radio is selected, but disabled. groupValue = 0; await tester.pumpWidget(buildApp(enabled: false)); 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: activeDisabledBackgroundColor), ); // Check when the radio is unselected and disabled. groupValue = 1; await tester.pumpWidget(buildApp(enabled: false)); 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: inactiveDisabledBackgroundColor), ); }); testWidgets('Radio background color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredBackgroundColor = Color(0xFF000001); const focusedBackgroundColor = Color(0xFF000002); Color getBackgroundColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredBackgroundColor; } if (states.contains(WidgetState.focused)) { return focusedBackgroundColor; } return Colors.transparent; } final WidgetStateProperty backgroundColor = WidgetStateColor.resolveWith( getBackgroundColor, ); int? groupValue = 0; const radioKey = Key('radio'); final theme = ThemeData(); Widget buildApp() { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: RadioGroup( groupValue: groupValue, onChanged: (int? newValue) { setState(() { groupValue = newValue; }); }, child: Radio( autofocus: true, focusNode: focusNode, key: radioKey, value: 0, backgroundColor: backgroundColor, ), ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect() ..circle(color: theme.colorScheme.primary.withValues(alpha: 0.1)) ..circle(color: focusedBackgroundColor), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byKey(radioKey))); 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: theme.colorScheme.primary.withValues(alpha: 0.08)) ..circle(color: hoveredBackgroundColor), ); focusNode.dispose(); }); testWidgets('Radio button side resolves in enabled/disabled states', (WidgetTester tester) async { const activeEnabledSide = BorderSide(color: Color(0xFF000001)); const activeDisabledSide = BorderSide(color: Color(0xFF000002), width: 2); const inactiveEnabledSide = BorderSide(color: Color(0xFF000003), width: 3); const inactiveDisabledSide = BorderSide(color: Color(0xFF000004), width: 4); BorderSide getSide(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledSide; } return inactiveDisabledSide; } if (states.contains(WidgetState.selected)) { return activeEnabledSide; } return inactiveEnabledSide; } final side = WidgetStateBorderSide.resolveWith(getSide); int? groupValue = 0; const radioKey = Key('radio'); Widget buildApp({required bool enabled}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: RadioGroup( groupValue: groupValue, onChanged: (int? newValue) { setState(() { groupValue = newValue; }); }, child: Radio(key: radioKey, value: 0, side: side, enabled: enabled), ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp(enabled: true)); // Selected and enabled. 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.transparent) ..circle(color: activeEnabledSide.color, strokeWidth: activeEnabledSide.width), ); // Check when the radio isn't selected. groupValue = 1; await tester.pumpWidget(buildApp(enabled: true)); 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.transparent) ..circle(color: inactiveEnabledSide.color, strokeWidth: inactiveEnabledSide.width), ); // Check when the radio is selected, but disabled. groupValue = 0; await tester.pumpWidget(buildApp(enabled: false)); 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.transparent) ..circle(color: activeDisabledSide.color, strokeWidth: activeDisabledSide.width), ); // Check when the radio is unselected and disabled. groupValue = 1; await tester.pumpWidget(buildApp(enabled: false)); 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.transparent) ..circle(color: inactiveDisabledSide.color, strokeWidth: inactiveDisabledSide.width), ); }); testWidgets('Radio background color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredSide = BorderSide(color: Color(0xFF000001)); const focusedSide = BorderSide(color: Color(0xFF000002), width: 2); BorderSide? getSide(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredSide; } if (states.contains(WidgetState.focused)) { return focusedSide; } return null; } final side = WidgetStateBorderSide.resolveWith(getSide); int? groupValue = 0; const radioKey = Key('radio'); final theme = ThemeData(); Widget buildApp() { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: RadioGroup( groupValue: groupValue, onChanged: (int? newValue) { setState(() { groupValue = newValue; }); }, child: Radio( autofocus: true, focusNode: focusNode, key: radioKey, value: 0, side: side, ), ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect() ..circle(color: theme.colorScheme.primary.withValues(alpha: 0.1)) ..circle(color: Colors.transparent) ..circle(color: focusedSide.color, strokeWidth: focusedSide.width), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byKey(radioKey))); 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: theme.colorScheme.primary.withValues(alpha: 0.08)) ..circle(color: Colors.transparent) ..circle(color: hoveredSide.color, strokeWidth: hoveredSide.width), ); focusNode.dispose(); }); testWidgets('Radio button inner radius resolves in enabled/disabled states', ( WidgetTester tester, ) async { const double enabledInnerRadius = 1; const double disabledInnerRadius = 2; double getInnerRadius(Set states) { if (states.contains(WidgetState.disabled)) { return disabledInnerRadius; } return enabledInnerRadius; } final WidgetStateProperty innerRadius = WidgetStateProperty.resolveWith(getInnerRadius); const value = 0; const radioKey = Key('radio'); Widget buildApp({required bool enabled}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: RadioGroup( groupValue: value, onChanged: (int? newValue) {}, child: Radio( key: radioKey, value: value, innerRadius: innerRadius, enabled: enabled, ), ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp(enabled: true)); // Enabled. 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.transparent) ..circle(color: theme.colorScheme.primary) ..circle(radius: enabledInnerRadius, color: theme.colorScheme.primary), ); // Disabled. await tester.pumpWidget(buildApp(enabled: false)); 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.transparent) ..circle(color: theme.colorScheme.onSurface.withAlpha(97)) ..circle(radius: disabledInnerRadius, color: theme.colorScheme.onSurface.withAlpha(97)), ); }); testWidgets('Radio inner radius resolves in hovered/focused states', (WidgetTester tester) async { final focusNode = FocusNode(debugLabel: 'radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const double hoveredInnerRadius = 1; const double focusedInnerRadius = 2; double? getInnerRadius(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredInnerRadius; } if (states.contains(WidgetState.focused)) { return focusedInnerRadius; } return null; } final WidgetStateProperty innerRadius = WidgetStateProperty.resolveWith( getInnerRadius, ); const value = 0; const radioKey = Key('radio'); final theme = ThemeData(); Widget buildApp() { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: 100, height: 100, color: Colors.white, child: RadioGroup( groupValue: value, onChanged: (int? newValue) {}, child: Radio( autofocus: true, focusNode: focusNode, key: radioKey, value: value, innerRadius: innerRadius, ), ), ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), paints ..rect() ..circle(color: theme.colorScheme.primary.withValues(alpha: 0.1)) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary) ..circle(radius: focusedInnerRadius, color: theme.colorScheme.primary), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byKey(radioKey))); 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: theme.colorScheme.primary.withValues(alpha: 0.08)) ..circle(color: Colors.transparent) ..circle(color: theme.colorScheme.primary) ..circle(radius: hoveredInnerRadius, color: theme.colorScheme.primary), ); focusNode.dispose(); }); // Regression tests for https://github.com/flutter/flutter/issues/170422 group('Radio accessibility announcements on various platforms', () { testWidgets('Unselected radio should be vocalized via hint on iOS/macOS platform', ( WidgetTester tester, ) async { const WidgetsLocalizations localizations = DefaultWidgetsLocalizations(); await tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: RadioGroup( groupValue: 2, onChanged: (int? value) {}, child: const Radio(value: 1), ), ), ), ); final SemanticsNode semanticNode = tester.getSemantics(find.byType(Focus).last); if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { expect(semanticNode.hint, localizations.radioButtonUnselectedLabel); } else { expect(semanticNode.hint, anyOf(isNull, isEmpty)); } }); testWidgets('Selected radio should be vocalized via the selected flag on all platforms', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: RadioGroup( groupValue: 1, onChanged: (int? value) {}, child: const Radio(value: 1), ), ), ), ); final SemanticsNode semanticNode = tester.getSemantics(find.byType(Focus).last); // Radio semantics should not have hint. expect(semanticNode.hint, anyOf(isNull, isEmpty)); }); }); testWidgets('Radio does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: Center(child: SizedBox.shrink(child: Radio(value: true))), ), ), ); expect(tester.getSize(find.byType(Radio)), Size.zero); }); }