// 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:async'; import 'dart:ui' as ui; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; void main() { final theme = ThemeData(); testWidgets('Switch can toggle on tap', (WidgetTester tester) async { final Key switchKey = UniqueKey(); var value = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return MaterialApp( theme: theme, home: Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, key: switchKey, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ), ); }, ), ), ); expect(value, isFalse); await tester.tap(find.byKey(switchKey)); expect(value, isTrue); }); testWidgets('Switch size is configurable by ThemeData.materialTapTargetSize', ( WidgetTester tester, ) async { final bool material3 = theme.useMaterial3; await tester.pumpWidget( Theme( data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded), child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (bool newValue) {}, ), ), ), ), ), ); // switch width = trackWidth - 2 * trackRadius + _kSwitchMinSize // M2 width = 33 - 2 * 7 + 40 // M3 width = 52 - 2 * 16 + 40 expect( tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 48.0) : const Size(59.0, 48.0), ); await tester.pumpWidget( Theme( data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (bool newValue) {}, ), ), ), ), ), ); expect( tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 40.0) : const Size(59.0, 40.0), ); }); testWidgets('Material2 - Switch does not get distorted upon changing constraints with parent', ( WidgetTester tester, ) async { const double maxWidth = 300; const double maxHeight = 100; const boundaryKey = ValueKey('switch container'); Widget buildSwitch({required double width, required double height}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: Directionality( textDirection: TextDirection.ltr, child: SizedBox( width: maxWidth, height: maxHeight, child: RepaintBoundary( key: boundaryKey, child: SizedBox( width: width, height: height, child: Switch( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (_) {}, ), ), ), ), ), ), ); } await tester.pumpWidget(buildSwitch(width: maxWidth, height: maxHeight)); await expectLater(find.byKey(boundaryKey), matchesGoldenFile('m2_switch_test.big.on.png')); await tester.pumpWidget(buildSwitch(width: 20, height: 10)); await expectLater(find.byKey(boundaryKey), matchesGoldenFile('m2_switch_test.small.on.png')); }); testWidgets('Material3 - Switch does not get distorted upon changing constraints with parent', ( WidgetTester tester, ) async { const double maxWidth = 300; const double maxHeight = 100; const boundaryKey = ValueKey('switch container'); Widget buildSwitch({required double width, required double height}) { return MaterialApp( home: Scaffold( body: Directionality( textDirection: TextDirection.ltr, child: SizedBox( width: maxWidth, height: maxHeight, child: RepaintBoundary( key: boundaryKey, child: SizedBox( width: width, height: height, child: Switch( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (_) {}, ), ), ), ), ), ), ); } await tester.pumpWidget(buildSwitch(width: maxWidth, height: maxHeight)); await expectLater(find.byKey(boundaryKey), matchesGoldenFile('m3_switch_test.big.on.png')); await tester.pumpWidget(buildSwitch(width: 20, height: 10)); await expectLater(find.byKey(boundaryKey), matchesGoldenFile('m3_switch_test.small.on.png')); }); testWidgets('Switch can drag (LTR)', (WidgetTester tester) async { var value = false; await tester.pumpWidget( Theme( data: theme, child: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, isFalse); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); expect(value, isFalse); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); expect(value, isTrue); await tester.pump(); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); expect(value, isTrue); await tester.pump(); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); expect(value, isFalse); }); testWidgets('Switch can drag with dragStartBehavior', (WidgetTester tester) async { var value = false; await tester.pumpWidget( Theme( data: theme, child: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, isFalse); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); expect(value, isFalse); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); expect(value, isTrue); await tester.pump(); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); expect(value, isTrue); await tester.pump(); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); expect(value, isFalse); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ); await tester.pumpAndSettle(); final Rect switchRect = tester.getRect(find.byType(Switch)); TestGesture gesture = await tester.startGesture(switchRect.center); // We have to execute the drag in two frames because the first update will // just set the start position. await gesture.moveBy(const Offset(20.0, 0.0)); await gesture.moveBy(const Offset(20.0, 0.0)); expect(value, isFalse); await gesture.up(); expect(value, isTrue); await tester.pump(); gesture = await tester.startGesture(switchRect.center); await gesture.moveBy(const Offset(20.0, 0.0)); await gesture.moveBy(const Offset(20.0, 0.0)); expect(value, isTrue); await gesture.up(); expect(value, isTrue); await tester.pump(); gesture = await tester.startGesture(switchRect.center); await gesture.moveBy(const Offset(-20.0, 0.0)); await gesture.moveBy(const Offset(-20.0, 0.0)); expect(value, isTrue); await gesture.up(); expect(value, isFalse); }); testWidgets('Switch can drag (RTL)', (WidgetTester tester) async { var value = false; await tester.pumpWidget( Theme( data: theme, child: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); expect(value, isFalse); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); expect(value, isTrue); await tester.pump(); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); expect(value, isTrue); await tester.pump(); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); expect(value, isFalse); }); testWidgets('Material2 - Switch has default colors when enabled', (WidgetTester tester) async { var value = false; await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x52000000), // Black with 32% opacity rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: Colors.grey.shade50), reason: 'Inactive enabled switch should match these colors', ); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x802196f3), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xff2196f3)), reason: 'Active enabled switch should match these colors', ); }); testWidgets('Material3 - Switch has default colors when enabled', (WidgetTester tester) async { final theme = ThemeData(); final ColorScheme colors = theme.colorScheme; var value = false; await tester.pumpWidget( MaterialApp( theme: theme, home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..save() ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect( style: PaintingStyle.stroke, color: colors.outline, rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)), ) ..rrect(color: colors.outline), // thumb color reason: 'Inactive enabled switch should match these colors', ); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..save() ..rrect( style: PaintingStyle.fill, color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: colors.onPrimary), // thumb color reason: 'Active enabled switch should match these colors', ); }); testWidgets('Switch.adaptive(Cupertino) has default colors when enabled', ( WidgetTester tester, ) async { final theme = ThemeData(); final ColorScheme colors = theme.colorScheme; var value = false; await tester.pumpWidget( MaterialApp( theme: theme, home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch.adaptive( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..save() ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect( style: PaintingStyle.stroke, color: colors.outline, rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)), ) ..rrect(color: colors.outline), // thumb color reason: 'Inactive enabled switch should match these colors', ); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..save() ..rrect( style: PaintingStyle.fill, color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: colors.onPrimary), // thumb color reason: 'Active enabled switch should match these colors', ); }); testWidgets('Material2 - Switch has default colors when disabled', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: const Directionality( textDirection: TextDirection.rtl, child: Material(child: Center(child: Switch(value: false, onChanged: null))), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.black12, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: Colors.grey.shade400), reason: 'Inactive disabled switch should match these colors', ); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: const Directionality( textDirection: TextDirection.rtl, child: Material(child: Center(child: Switch(value: true, onChanged: null))), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.black12, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: Colors.grey.shade400), reason: 'Active disabled switch should match these colors', ); }); testWidgets('Material3 - Inactive Switch has default colors when disabled', ( WidgetTester tester, ) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; await tester.pumpWidget( MaterialApp( theme: themeData, home: const Directionality( textDirection: TextDirection.rtl, child: Material(child: Center(child: Switch(value: false, onChanged: null))), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..save() ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect( style: PaintingStyle.stroke, color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)), ) ..rrect( color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface), ), // thumb color reason: 'Inactive disabled switch should match these colors', ); }); testWidgets('Material3 - Active Switch has default colors when disabled', ( WidgetTester tester, ) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; await tester.pumpWidget( MaterialApp( theme: themeData, home: const Directionality( textDirection: TextDirection.rtl, child: Material(child: Center(child: Switch(value: true, onChanged: null))), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..save() ..rrect( style: PaintingStyle.fill, color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: colors.surface), // thumb color reason: 'Active disabled switch should match these colors', ); }); testWidgets('Material2 - Switch default overlayColor resolves hovered/focused state', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; Finder findSwitch() { return find.byWidgetPredicate((Widget widget) => widget is Switch); } MaterialInkController? getSwitchMaterial(WidgetTester tester) { return Material.of(tester.element(findSwitch())); } await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: Switch(focusNode: focusNode, value: true, onChanged: (_) {}), ), ), ); // Focused. focusNode.requestFocus(); await tester.pumpAndSettle(); expect(getSwitchMaterial(tester), paints..circle(color: theme.focusColor)); // On both hovered and focused, the overlay color should show hovered overlay color. final Offset center = tester.getCenter(find.byType(Switch)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(center); await tester.pumpAndSettle(); expect(getSwitchMaterial(tester), paints..circle(color: theme.hoverColor)); focusNode.dispose(); }); testWidgets('Material3 - Switch default overlayColor resolves hovered/focused state', ( WidgetTester tester, ) async { final theme = ThemeData(); final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; Finder findSwitch() { return find.byWidgetPredicate((Widget widget) => widget is Switch); } MaterialInkController? getSwitchMaterial(WidgetTester tester) { return Material.of(tester.element(findSwitch())); } await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Switch(focusNode: focusNode, value: true, onChanged: (_) {}), ), ), ); // Focused. focusNode.requestFocus(); await tester.pumpAndSettle(); expect( getSwitchMaterial(tester), paints..circle(color: theme.colorScheme.primary.withOpacity(0.1)), ); // On both hovered and focused, the overlay color should show hovered overlay color. final Offset center = tester.getCenter(find.byType(Switch)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(center); await tester.pumpAndSettle(); expect( getSwitchMaterial(tester), paints..circle(color: theme.colorScheme.primary.withOpacity(0.08)), ); focusNode.dispose(); }); testWidgets('Material2 - Switch can be set color', (WidgetTester tester) async { var value = false; await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, activeColor: Colors.red[500], activeTrackColor: Colors.green[500], inactiveThumbColor: Colors.yellow[500], inactiveTrackColor: Colors.blue[500], ), ), ); }, ), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.blue[500], rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: Colors.yellow[500]), ); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.green[500], rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: Colors.red[500]), ); }); testWidgets('Material3 - Switch can be set color', (WidgetTester tester) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; var value = false; await tester.pumpWidget( MaterialApp( theme: themeData, home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, activeColor: Colors.red[500], activeTrackColor: Colors.green[500], inactiveThumbColor: Colors.yellow[500], inactiveTrackColor: Colors.blue[500], ), ), ); }, ), ), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: Colors.blue[500], rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect(style: PaintingStyle.stroke, color: colors.outline) ..rrect(color: Colors.yellow[500]), // thumb color ); await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: Colors.green[500], rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: Colors.red[500]), // thumb color ); }); testWidgets('Drag ends after animation completes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/17773 var value = false; await tester.pumpWidget( MaterialApp( theme: theme, home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, isFalse); final Rect switchRect = tester.getRect(find.byType(Switch)); final TestGesture gesture = await tester.startGesture(switchRect.centerLeft); await tester.pump(); await gesture.moveBy(Offset(switchRect.width, 0.0)); await tester.pump(); await gesture.up(); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); expect(value, isTrue); expect(tester.hasRunningAnimations, false); }); testWidgets('can veto switch dragging result', (WidgetTester tester) async { var value = false; await tester.pumpWidget( Theme( data: theme, child: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: value, onChanged: (bool newValue) { setState(() { value = value || newValue; }); }, ), ), ); }, ), ), ), ); // Move a little to the right, not past the middle. TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center); await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0)); await tester.pump(); await gesture.moveBy(const Offset(-kTouchSlop + 5.1, 0.0)); await tester.pump(); await gesture.up(); await tester.pump(); expect(value, isFalse); final ToggleableStateMixin state = tester.state( find.descendant( of: find.byType(Switch), matching: find.byWidgetPredicate( (Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch', ), ), ); expect(state.position.value, lessThan(0.5)); await tester.pump(); await tester.pumpAndSettle(); expect(value, isFalse); expect(state.position.value, 0); // Move past the middle. gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center); await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0)); await tester.pump(); await gesture.up(); await tester.pump(); expect(value, isTrue); expect(state.position.value, greaterThan(0.5)); await tester.pump(); await tester.pumpAndSettle(); expect(value, isTrue); expect(state.position.value, 1.0); // Now move back to the left, the revert animation should play. gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center); await gesture.moveBy(const Offset(-kTouchSlop - 0.1, 0.0)); await tester.pump(); await gesture.up(); await tester.pump(); expect(value, isTrue); expect(state.position.value, lessThan(0.5)); await tester.pump(); await tester.pumpAndSettle(); expect(value, isTrue); expect(state.position.value, 1.0); }); testWidgets('switch has semantic events', (WidgetTester tester) async { dynamic semanticEvent; var value = false; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler( SystemChannels.accessibility, (dynamic message) async { semanticEvent = message; }, ); final semanticsTester = SemanticsTester(tester); await tester.pumpWidget( Theme( data: theme, child: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); await tester.tap(find.byType(Switch)); final RenderObject object = tester.firstRenderObject(find.byType(Switch)); expect(value, true); expect(semanticEvent, { 'type': 'tap', 'nodeId': object.debugSemantics!.id, 'data': {}, }); expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true); semanticsTester.dispose(); tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler( SystemChannels.accessibility, null, ); }); testWidgets('switch sends semantic events from parent if fully merged', ( WidgetTester tester, ) async { dynamic semanticEvent; var value = false; tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler( SystemChannels.accessibility, (dynamic message) async { semanticEvent = message; }, ); final semanticsTester = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( theme: theme, home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { void onChanged(bool newValue) { setState(() { value = newValue; }); } return Material( child: MergeSemantics( child: ListTile( leading: const Text('test'), onTap: () { onChanged(!value); }, trailing: Switch(value: value, onChanged: onChanged), ), ), ); }, ), ), ); await tester.tap(find.byType(MergeSemantics)); final RenderObject object = tester.firstRenderObject(find.byType(MergeSemantics)); expect(value, true); expect(semanticEvent, { 'type': 'tap', 'nodeId': object.debugSemantics!.id, 'data': {}, }); expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true); semanticsTester.dispose(); tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler( SystemChannels.accessibility, null, ); }); testWidgets('Switch.adaptive', (WidgetTester tester) async { var value = false; const activeTrackColor = Color(0xffff1200); const inactiveTrackColor = Color(0xffff12ff); const thumbColor = Color(0xffffff00); const focusColor = Color(0xff00ff00); Widget buildFrame(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch.adaptive( value: value, activeColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, thumbColor: const MaterialStatePropertyAll(thumbColor), focusColor: focusColor, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ); } for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { value = false; await tester.pumpWidget(buildFrame(platform)); expect(find.byType(Switch), findsOneWidget, reason: 'on ${platform.name}'); expect(find.byType(CupertinoSwitch), findsNothing); final Switch adaptiveSwitch = tester.widget(find.byType(Switch)); expect(adaptiveSwitch.activeColor, activeTrackColor, reason: 'on ${platform.name}'); expect(adaptiveSwitch.inactiveTrackColor, inactiveTrackColor, reason: 'on ${platform.name}'); expect( adaptiveSwitch.thumbColor?.resolve({}), thumbColor, reason: 'on ${platform.name}', ); expect(adaptiveSwitch.focusColor, focusColor, reason: 'on ${platform.name}'); expect(value, isFalse, reason: 'on ${platform.name}'); await tester.tap(find.byType(Switch)); expect(value, isTrue, reason: 'on ${platform.name}'); } for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { value = false; await tester.pumpWidget(buildFrame(platform)); await tester.pumpAndSettle(); // Finish the theme change animation. expect(find.byType(CupertinoSwitch), findsNothing); expect(value, isFalse, reason: 'on ${platform.name}'); await tester.tap(find.byType(Switch)); expect(value, isTrue, reason: 'on ${platform.name}'); } }); testWidgets('Switch.adaptive default mouse cursor(Cupertino)', (WidgetTester tester) async { for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { await tester.pumpWidget(buildAdaptiveSwitch(platform: platform, value: false)); final Size switchSize = tester.getSize(find.byType(Switch)); expect(switchSize, const Size(60.0, 48.0)); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.addPointer(location: tester.getCenter(find.byType(Switch))); addTearDown(gesture.removePointer); await tester.pump(); await gesture.moveTo(tester.getCenter(find.byType(Switch))); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); await tester.pumpWidget(buildAdaptiveSwitch(platform: platform)); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); // Test disabled switch. await tester.pumpWidget( buildAdaptiveSwitch(platform: platform, enabled: false, value: false), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); await gesture.removePointer(location: tester.getCenter(find.byType(Switch))); await tester.pump(); } }); testWidgets('Switch.adaptive default thumb/track color and size(Cupertino)', ( WidgetTester tester, ) async { const Color thumbColor = Colors.white; const inactiveTrackColor = Color.fromARGB(40, 120, 120, 128); // Default inactive track color. const activeTrackColor = Color.fromARGB(255, 52, 199, 89); // Default active track color. for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { // Switches have same sizes on both platform but they are more compact on macOS. final trackRRect = platform == TargetPlatform.iOS ? RRect.fromLTRBR(4.5, 8.5, 55.5, 39.5, const Radius.circular(15.5)) : RRect.fromLTRBR(4.5, 4.5, 55.5, 35.5, const Radius.circular(15.5)); final inactiveThumbRRect = platform == TargetPlatform.iOS ? RRect.fromLTRBR(6.0, 10.0, 34.0, 38.0, const Radius.circular(14.0)) : RRect.fromLTRBR(6.0, 6.0, 34.0, 34.0, const Radius.circular(14.0)); final activeThumbRRect = platform == TargetPlatform.iOS ? RRect.fromLTRBR(26.0, 10.0, 54.0, 38.0, const Radius.circular(14.0)) : RRect.fromLTRBR(26.0, 6.0, 54.0, 34.0, const Radius.circular(14.0)); await tester.pumpWidget(Container()); await tester.pumpWidget(buildAdaptiveSwitch(platform: platform, value: false)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: inactiveTrackColor, rrect: trackRRect, ) // Default cupertino inactive track color ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: thumbColor, rrect: inactiveThumbRRect), reason: 'Inactive enabled switch should have default track and thumb color', ); expect(find.byType(Opacity), findsOneWidget); expect(tester.widget(find.byType(Opacity)).opacity, 1.0); await tester.pumpWidget(Container()); await tester.pumpWidget(buildAdaptiveSwitch(platform: platform)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: activeTrackColor, rrect: trackRRect, ) // Default cupertino active track color ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: thumbColor, rrect: activeThumbRRect), reason: 'Active enabled switch should have default track and thumb color', ); expect(find.byType(Opacity), findsOneWidget); expect(tester.widget(find.byType(Opacity)).opacity, 1.0); // Test disabled switch. await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch(platform: platform, enabled: false, value: false), ); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: inactiveTrackColor, rrect: trackRRect, ) // Default cupertino inactive track color ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: thumbColor, rrect: inactiveThumbRRect), reason: 'Inactive disabled switch should have default track and thumb color', ); expect(find.byType(Opacity), findsOneWidget); expect(tester.widget(find.byType(Opacity)).opacity, 0.5); await tester.pumpWidget(Container()); await tester.pumpWidget(buildAdaptiveSwitch(platform: platform, enabled: false)); await tester.pump(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: activeTrackColor, rrect: trackRRect, ) // Default cupertino active track color ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: thumbColor, rrect: activeThumbRRect), reason: 'Active disabled switch should have default track and thumb color', ); expect(find.byType(Opacity), findsOneWidget); expect(tester.widget(find.byType(Opacity)).opacity, 0.5); } }); testWidgets('Default Switch.adaptive are not affected by ' 'ThemeData.switchThemeData on iOS/macOS', (WidgetTester tester) async { const Color defaultThumbColor = Colors.white; const defaultInactiveTrackColor = Color.fromARGB(40, 120, 120, 128); const defaultActiveTrackColor = Color.fromARGB(255, 52, 199, 89); const Color updatedThumbColor = Colors.red; const Color updatedTrackColor = Colors.green; const overallSwitchTheme = SwitchThemeData( thumbColor: MaterialStatePropertyAll(updatedThumbColor), trackColor: MaterialStatePropertyAll(updatedTrackColor), ); for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch(platform: platform, overallSwitchThemeData: overallSwitchTheme), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: defaultActiveTrackColor) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: defaultThumbColor), reason: 'Active enabled switch should still have default track and thumb color', ); await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch( platform: platform, value: false, overallSwitchThemeData: overallSwitchTheme, ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: defaultInactiveTrackColor) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: defaultThumbColor), reason: 'Inactive enabled switch should have default track and thumb color', ); await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch( platform: platform, enabled: false, value: false, overallSwitchThemeData: overallSwitchTheme, ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: defaultInactiveTrackColor) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: defaultThumbColor), reason: 'Inactive disabled switch should have default track and thumb color', ); } await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch( platform: TargetPlatform.android, overallSwitchThemeData: overallSwitchTheme, ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: Color(updatedTrackColor.value)) ..rrect() ..rrect(color: Color(updatedThumbColor.value)), reason: 'Switch.adaptive is affected by SwitchTheme on other platforms', ); }); testWidgets('Default Switch.adaptive are not affected by ' 'SwitchThemeData on iOS/macOS', (WidgetTester tester) async { const Color defaultThumbColor = Colors.white; const defaultInactiveTrackColor = Color.fromARGB(40, 120, 120, 128); const defaultActiveTrackColor = Color.fromARGB(255, 52, 199, 89); const Color updatedThumbColor = Colors.red; const Color updatedTrackColor = Colors.green; const switchTheme = SwitchThemeData( thumbColor: MaterialStatePropertyAll(updatedThumbColor), trackColor: MaterialStatePropertyAll(updatedTrackColor), ); for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch(platform: platform, switchThemeData: switchTheme), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: defaultActiveTrackColor) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: defaultThumbColor), reason: 'Active enabled switch should still have default track and thumb color', ); await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch(platform: platform, value: false, switchThemeData: switchTheme), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: defaultInactiveTrackColor) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: defaultThumbColor), reason: 'Inactive enabled switch should have default track and thumb color', ); await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch( platform: platform, enabled: false, value: false, switchThemeData: switchTheme, ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: defaultInactiveTrackColor) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino) ..rrect(color: defaultThumbColor), reason: 'Inactive disabled switch should have default track and thumb color', ); } await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch(platform: TargetPlatform.android, switchThemeData: switchTheme), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: Color(updatedTrackColor.value)) ..rrect() ..rrect(color: Color(updatedThumbColor.value)), reason: 'Switch.adaptive is affected by SwitchTheme on other platforms', ); }); testWidgets('Override default adaptive SwitchThemeData on iOS/macOS', ( WidgetTester tester, ) async { for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch( platform: platform, switchThemeData: const SwitchThemeData( thumbColor: MaterialStatePropertyAll(Colors.yellow), trackColor: MaterialStatePropertyAll(Colors.brown), ), switchThemeAdaptation: const _SwitchThemeAdaptation(), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: Color(Colors.deepPurple.value)) ..rrect() ..rrect() ..rrect() ..rrect() ..rrect(color: Color(Colors.lightGreen.value)), ); } // Other platforms should not be affected by the adaptive switch theme. for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { await tester.pumpWidget(Container()); await tester.pumpWidget( buildAdaptiveSwitch( platform: platform, switchThemeData: const SwitchThemeData( thumbColor: MaterialStatePropertyAll(Colors.yellow), trackColor: MaterialStatePropertyAll(Colors.brown), ), switchThemeAdaptation: const _SwitchThemeAdaptation(), ), ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: Color(Colors.brown.value)) ..rrect() ..rrect(color: Color(Colors.yellow.value)), ); } }); testWidgets('Switch.adaptive default focus color(Cupertino)', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final node = FocusNode(); addTearDown(node.dispose); await tester.pumpWidget( buildAdaptiveSwitch(platform: TargetPlatform.macOS, autofocus: true, focusNode: node), ); await tester.pumpAndSettle(); expect(node.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: const Color(0xff34c759)) // Track color ..rrect() ..rrect( color: const Color(0xcc6ef28f), strokeWidth: 3.5, style: PaintingStyle.stroke, ) // Focused outline ..rrect() ..rrect() ..rrect() ..rrect(color: const Color(0xffffffff)), // Thumb color ); await tester.pumpWidget( buildAdaptiveSwitch( platform: TargetPlatform.macOS, autofocus: true, focusNode: node, focusColor: Colors.red, ), ); await tester.pumpAndSettle(); expect(node.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: const Color(0xff34c759)) // Track color ..rrect() ..rrect( color: Color(Colors.red.value), strokeWidth: 3.5, style: PaintingStyle.stroke, ) // Focused outline ..rrect() ..rrect() ..rrect() ..rrect(color: const Color(0xffffffff)), // Thumb color ); }); testWidgets('Material2 - Switch is focusable and has correct focus color', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Switch( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, focusColor: Colors.orange[500], autofocus: true, focusNode: focusNode, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x802196f3), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..circle(color: Colors.orange[500]) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xff2196f3)), ); // Check the false value. value = false; await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x52000000), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..circle(color: Colors.orange[500]) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xfffafafa)), ); // Check what happens when disabled. value = false; await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x1f000000), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xffbdbdbd)), ); focusNode.dispose(); }); testWidgets('Material3 - Switch is focusable and has correct focus color', ( WidgetTester tester, ) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: themeData, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Switch( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, focusColor: Colors.orange[500], autofocus: true, focusNode: focusNode, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // active, enabled switch await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..circle(color: Colors.orange[500]), ); // Check the false value: inactive enabled switch value = false; await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect( style: PaintingStyle.stroke, color: colors.outline, rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)), ) ..circle(color: Colors.orange[500]), ); // Check what happens when disabled: inactive disabled switch. value = false; await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect( style: PaintingStyle.stroke, color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)), ) ..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), ); focusNode.dispose(); }); testWidgets('Switch 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: Switch( value: true, onChanged: (bool newValue) {}, focusColor: Colors.orange[500], autofocus: true, splashRadius: splashRadius, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..circle(color: Colors.orange[500], radius: splashRadius), ); }); testWidgets('Material2 - Switch can be hovered and has correct hover color', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Switch( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, hoverColor: Colors.orange[500], ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x802196f3), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xff2196f3)), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x802196f3), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..circle(color: Colors.orange[500]) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xff2196f3)), ); // Check what happens when disabled. await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x1f000000), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0xffbdbdbd)), ); }); testWidgets('Material3 - Switch can be hovered and has correct hover color', ( WidgetTester tester, ) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: themeData, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Switch( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, hoverColor: Colors.orange[500], ); }, ), ), ), ); } // active enabled switch await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: colors.onPrimary), ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..circle(color: Colors.orange[500]), ); // Check what happens for disabled active switch await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: colors.surface.withOpacity(1.0)), ); }); testWidgets('Switch can be toggled by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Switch( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, focusColor: Colors.orange[500], autofocus: true, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pumpAndSettle(); // On web, switches don't respond to the enter key. expect(value, kIsWeb ? isTrue : isFalse); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pumpAndSettle(); expect(value, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(value, isFalse); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(value, isTrue); }); testWidgets('Switch changes mouse cursor when hovered', (WidgetTester tester) async { // Test Switch.adaptive() constructor await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Align( alignment: Alignment.topLeft, child: Material( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Switch.adaptive( mouseCursor: SystemMouseCursors.text, value: true, onChanged: (_) {}, ), ), ), ), ), ), ); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.addPointer(location: tester.getCenter(find.byType(Switch))); addTearDown(gesture.removePointer); await tester.pump(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text, ); // Test Switch() constructor await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( body: Align( alignment: Alignment.topLeft, child: Material( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Switch(mouseCursor: SystemMouseCursors.text, value: true, onChanged: (_) {}), ), ), ), ), ), ); await gesture.moveTo(tester.getCenter(find.byType(Switch))); 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: Switch(value: true, onChanged: (_) {}), ), ), ), ), ), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click, ); // 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: Switch(value: true, onChanged: null), ), ), ), ), ), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); await tester.pumpAndSettle(); }); testWidgets('Material switch should not recreate its render object when disabled', ( WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/61247. var value = true; var enabled = true; late StateSetter stateSetter; await tester.pumpWidget( Theme( data: theme, child: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; return Material( child: Center( child: Switch( value: value, onChanged: !enabled ? null : (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); final ToggleableStateMixin oldSwitchState = tester.state( find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'), ); stateSetter(() { value = false; }); await tester.pump(); // Disable the switch when the implicit animation begins. stateSetter(() { enabled = false; }); await tester.pump(); final ToggleableStateMixin updatedSwitchState = tester.state( find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'), ); expect(updatedSwitchState.isInteractive, false); expect(updatedSwitchState, oldSwitchState); expect(updatedSwitchState.position.isCompleted, false); expect(updatedSwitchState.position.isDismissed, false); }); testWidgets('Material2 - Switch thumb color resolves in active/enabled states', ( WidgetTester tester, ) async { const activeEnabledThumbColor = Color(0xFF000001); const activeDisabledThumbColor = Color(0xFF000002); const inactiveEnabledThumbColor = Color(0xFF000003); const inactiveDisabledThumbColor = Color(0xFF000004); Color getThumbColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledThumbColor; } return inactiveDisabledThumbColor; } if (states.contains(WidgetState.selected)) { return activeEnabledThumbColor; } return inactiveEnabledThumbColor; } final WidgetStateProperty thumbColor = WidgetStateColor.resolveWith(getThumbColor); Widget buildSwitch({required bool enabled, required bool active}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( thumbColor: thumbColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.black12, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: inactiveDisabledThumbColor), reason: 'Inactive disabled switch should default track and custom thumb color', ); await tester.pumpWidget(buildSwitch(enabled: false, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.black12, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: activeDisabledThumbColor), reason: 'Active disabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x52000000), // Black with 32% opacity, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: inactiveEnabledThumbColor), reason: 'Inactive enabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: false, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.black12, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: inactiveDisabledThumbColor), reason: 'Inactive disabled switch should match these colors', ); }); testWidgets('Material3 - Switch thumb color resolves in active/enabled states', ( WidgetTester tester, ) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; const activeEnabledThumbColor = Color(0xFF000001); const activeDisabledThumbColor = Color(0xFF000002); const inactiveEnabledThumbColor = Color(0xFF000003); const inactiveDisabledThumbColor = Color(0xFF000004); Color getThumbColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledThumbColor; } return inactiveDisabledThumbColor; } if (states.contains(WidgetState.selected)) { return activeEnabledThumbColor; } return inactiveEnabledThumbColor; } final WidgetStateProperty thumbColor = WidgetStateColor.resolveWith(getThumbColor); Widget buildSwitch({required bool enabled, required bool active}) { return Theme( data: themeData, child: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( thumbColor: thumbColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect( style: PaintingStyle.stroke, color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)), ) ..rrect(color: inactiveDisabledThumbColor), reason: 'Inactive disabled switch should default track and custom thumb color', ); await tester.pumpWidget(buildSwitch(enabled: false, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: activeDisabledThumbColor), reason: 'Active disabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.surfaceContainerHighest, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: inactiveEnabledThumbColor), reason: 'Inactive enabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: activeEnabledThumbColor), reason: 'Active enabled switch should match these colors', ); }); testWidgets('Material2 - Switch thumb color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredThumbColor = Color(0xFF000001); const focusedThumbColor = Color(0xFF000002); Color getThumbColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredThumbColor; } if (states.contains(WidgetState.focused)) { return focusedThumbColor; } return Colors.transparent; } final WidgetStateProperty thumbColor = WidgetStateColor.resolveWith(getThumbColor); Widget buildSwitch() { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Switch( focusNode: focusNode, autofocus: true, value: true, thumbColor: thumbColor, onChanged: (_) {}, ), ), ), ); } await tester.pumpWidget(buildSwitch()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x802196f3), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..circle() // Radial reaction ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: focusedThumbColor), reason: 'Inactive disabled switch should default track and custom thumb color', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: const Color(0x802196f3), rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..circle() ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: hoveredThumbColor), reason: 'Inactive disabled switch should default track and custom thumb color', ); focusNode.dispose(); }); testWidgets('Material3 - Switch thumb color resolves in hovered/focused states', ( WidgetTester tester, ) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredThumbColor = Color(0xFF000001); const focusedThumbColor = Color(0xFF000002); Color getThumbColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredThumbColor; } if (states.contains(WidgetState.focused)) { return focusedThumbColor; } return Colors.transparent; } final WidgetStateProperty thumbColor = WidgetStateColor.resolveWith(getThumbColor); Widget buildSwitch() { return MaterialApp( theme: themeData, home: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( focusNode: focusNode, autofocus: true, value: true, thumbColor: thumbColor, onChanged: (_) {}, ), ), ), ), ); } await tester.pumpWidget(buildSwitch()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..circle(color: colors.primary.withOpacity(0.1)) ..rrect(color: focusedThumbColor), reason: 'active enabled switch should default track and custom thumb color', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: colors.primary, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..circle(color: colors.primary.withOpacity(0.08)) ..rrect(color: hoveredThumbColor), reason: 'active enabled switch should default track and custom thumb color', ); focusNode.dispose(); }); testWidgets('Material2 - Track color resolves in active/enabled states', ( WidgetTester tester, ) async { const activeEnabledTrackColor = Color(0xFF000001); const activeDisabledTrackColor = Color(0xFF000002); const inactiveEnabledTrackColor = Color(0xFF000003); const inactiveDisabledTrackColor = Color(0xFF000004); Color getTrackColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledTrackColor; } return inactiveDisabledTrackColor; } if (states.contains(WidgetState.selected)) { return activeEnabledTrackColor; } return inactiveEnabledTrackColor; } final WidgetStateProperty trackColor = WidgetStateColor.resolveWith(getTrackColor); Widget buildSwitch({required bool enabled, required bool active}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Switch( trackColor: trackColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: inactiveDisabledTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ), reason: 'Inactive disabled switch track should use this value', ); await tester.pumpWidget(buildSwitch(enabled: false, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: activeDisabledTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ), reason: 'Active disabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: inactiveEnabledTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ), reason: 'Inactive enabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: false, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: inactiveDisabledTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ), reason: 'Inactive disabled switch should match these colors', ); }); testWidgets('Material3 - Track color resolves in active/enabled states', ( WidgetTester tester, ) async { final themeData = ThemeData(); const activeEnabledTrackColor = Color(0xFF000001); const activeDisabledTrackColor = Color(0xFF000002); const inactiveEnabledTrackColor = Color(0xFF000003); const inactiveDisabledTrackColor = Color(0xFF000004); Color getTrackColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledTrackColor; } return inactiveDisabledTrackColor; } if (states.contains(WidgetState.selected)) { return activeEnabledTrackColor; } return inactiveEnabledTrackColor; } final WidgetStateProperty trackColor = WidgetStateColor.resolveWith(getTrackColor); Widget buildSwitch({required bool enabled, required bool active}) { return Theme( data: themeData, child: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( trackColor: trackColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: inactiveDisabledTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ), reason: 'Inactive disabled switch track should use this value', ); await tester.pumpWidget(buildSwitch(enabled: false, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: activeDisabledTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ), reason: 'Active disabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: inactiveEnabledTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ), reason: 'Inactive enabled switch should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: activeEnabledTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ), reason: 'Active enabled switch should match these colors', ); }); testWidgets('Material2 - Switch track color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredTrackColor = Color(0xFF000001); const focusedTrackColor = Color(0xFF000002); Color getTrackColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredTrackColor; } if (states.contains(WidgetState.focused)) { return focusedTrackColor; } return Colors.transparent; } final WidgetStateProperty trackColor = WidgetStateColor.resolveWith(getTrackColor); Widget buildSwitch() { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( focusNode: focusNode, autofocus: true, value: true, trackColor: trackColor, onChanged: (_) {}, ), ), ), ), ); } await tester.pumpWidget(buildSwitch()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: focusedTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ), reason: 'Inactive enabled switch should match these colors', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: hoveredTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ), reason: 'Inactive enabled switch should match these colors', ); focusNode.dispose(); }); testWidgets('Material3 - Switch track color resolves in hovered/focused states', ( WidgetTester tester, ) async { final themeData = ThemeData(); final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredTrackColor = Color(0xFF000001); const focusedTrackColor = Color(0xFF000002); Color getTrackColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredTrackColor; } if (states.contains(WidgetState.focused)) { return focusedTrackColor; } return Colors.transparent; } final WidgetStateProperty trackColor = WidgetStateColor.resolveWith(getTrackColor); Widget buildSwitch() { return Theme( data: themeData, child: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( focusNode: focusNode, autofocus: true, value: true, trackColor: trackColor, onChanged: (_) {}, ), ), ), ), ); } await tester.pumpWidget(buildSwitch()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: focusedTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ), reason: 'Active enabled switch should match these colors', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints..rrect( color: hoveredTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ), reason: 'Active enabled switch should match these colors', ); focusNode.dispose(); }); testWidgets('Material2 - Switch thumb color is blended against surface color', ( WidgetTester tester, ) async { final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60); final theme = ThemeData.light(useMaterial3: false); Color getThumbColor(Set states) { if (states.contains(WidgetState.disabled)) { return activeDisabledThumbColor; } return Colors.black; } final WidgetStateProperty thumbColor = WidgetStateColor.resolveWith(getThumbColor); Widget buildSwitch({required bool enabled, required bool active}) { return Directionality( textDirection: TextDirection.rtl, child: Theme( data: theme, child: Material( child: Center( child: Switch( thumbColor: thumbColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: true)); final Color expectedThumbColor = Color.alphaBlend( activeDisabledThumbColor, theme.colorScheme.surface, ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: Colors.black12, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: expectedThumbColor), reason: 'Active disabled thumb color should be blended on top of surface color', ); }); testWidgets('Material3 - Switch thumb color is blended against surface color', ( WidgetTester tester, ) async { final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60); final theme = ThemeData(); final ColorScheme colors = theme.colorScheme; Color getThumbColor(Set states) { if (states.contains(WidgetState.disabled)) { return activeDisabledThumbColor; } return Colors.black; } final WidgetStateProperty thumbColor = WidgetStateColor.resolveWith(getThumbColor); Widget buildSwitch({required bool enabled, required bool active}) { return Directionality( textDirection: TextDirection.rtl, child: Theme( data: theme, child: Material( child: Center( child: Switch( thumbColor: thumbColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: true)); final Color expectedThumbColor = Color.alphaBlend( activeDisabledThumbColor, theme.colorScheme.surface, ); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.onSurface.withOpacity(0.12), rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: expectedThumbColor), reason: 'Active disabled thumb color should be blended on top of surface color', ); }); testWidgets('Switch overlay color resolves in active/pressed/focused/hovered states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const activeThumbColor = Color(0xFF000000); const inactiveThumbColor = Color(0xFF000010); 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; Widget buildSwitch({bool active = false, bool focused = false, bool useOverlay = true}) { return MaterialApp( theme: theme, home: Scaffold( body: Switch( focusNode: focusNode, autofocus: focused, value: active, onChanged: (_) {}, thumbColor: WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.selected)) { return activeThumbColor; } return inactiveThumbColor; }), overlayColor: useOverlay ? WidgetStateProperty.resolveWith(getOverlayColor) : null, hoverColor: hoverColor, focusColor: focusColor, splashRadius: splashRadius, ), ), ); } // test inactive Switch, and overlayColor is set to null. await tester.pumpWidget(buildSwitch(useOverlay: false)); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..circle(color: inactiveThumbColor.withAlpha(kRadialReactionAlpha), radius: splashRadius), reason: 'Default inactive pressed Switch should have overlay color from thumbColor', ); // test active Switch, and overlayColor is set to null. await tester.pumpWidget(buildSwitch(active: true, useOverlay: false)); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..circle(color: activeThumbColor.withAlpha(kRadialReactionAlpha), radius: splashRadius), reason: 'Default active pressed Switch should have overlay color from thumbColor', ); // test inactive Switch with an overlayColor await tester.pumpWidget(buildSwitch()); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..circle(color: inactivePressedOverlayColor, radius: splashRadius), reason: 'Inactive pressed Switch should have overlay color: $inactivePressedOverlayColor', ); // test active Switch with an overlayColor await tester.pumpWidget(buildSwitch(active: true)); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..circle(color: activePressedOverlayColor, radius: splashRadius), reason: 'Active pressed Switch should have overlay color: $activePressedOverlayColor', ); await tester.pumpWidget(buildSwitch(focused: true)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..circle(color: focusOverlayColor, radius: splashRadius), reason: 'Focused Switch 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(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..circle(color: hoverOverlayColor, radius: splashRadius), reason: 'Hovered Switch should use overlay color $hoverOverlayColor over $hoverColor', ); focusNode.dispose(); }); testWidgets('Do not crash when widget disappears while pointer is down', ( WidgetTester tester, ) async { Widget buildSwitch(bool show) { return MaterialApp( theme: theme, home: Material( child: Center(child: show ? Switch(value: true, onChanged: (_) {}) : Container()), ), ); } await tester.pumpWidget(buildSwitch(true)); final Offset center = tester.getCenter(find.byType(Switch)); // 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(buildSwitch(false)); expect(find.byType(Switch), findsNothing); // Release pointer after widget disappeared. await gesture.up(); }); testWidgets('disabled switch shows tooltip', (WidgetTester tester) async { const longPressTooltip = 'long press tooltip'; const tapTooltip = 'tap tooltip'; await tester.pumpWidget( const MaterialApp( home: Material( child: Tooltip(message: longPressTooltip, child: Switch(onChanged: null, value: true)), ), ), ); // 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( const MaterialApp( home: Material( child: Tooltip( triggerMode: TooltipTriggerMode.tap, message: tapTooltip, child: Switch(onChanged: null, value: true), ), ), ), ); 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); }); group('with image', () { late ui.Image image; setUp(() async { image = await createTestImage(width: 100, height: 100); }); testWidgets('thumb image shows up', (WidgetTester tester) async { imageCache.clear(); final provider1 = _TestImageProvider(); final provider2 = _TestImageProvider(); expect(provider1.loadCallCount, 0); expect(provider2.loadCallCount, 0); var value1 = true; await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Switch( activeThumbImage: provider1, inactiveThumbImage: provider2, value: value1, onChanged: (bool val) { setState(() { value1 = val; }); }, ), ); }, ), ), ); expect(provider1.loadCallCount, 1); expect(provider2.loadCallCount, 0); expect(imageCache.liveImageCount, 1); await tester.tap(find.byType(Switch)); await tester.pumpAndSettle(); expect(provider1.loadCallCount, 1); expect(provider2.loadCallCount, 1); expect(imageCache.liveImageCount, 2); }); testWidgets('do not crash when imageProvider completes after Switch is disposed', ( WidgetTester tester, ) async { final imageProvider = DelayedImageProvider(image); await tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: Center( child: Switch(value: true, onChanged: null, inactiveThumbImage: imageProvider), ), ), ), ); expect(find.byType(Switch), findsOneWidget); // Dispose the switch by taking down the tree. await tester.pumpWidget(Container()); expect(find.byType(Switch), findsNothing); imageProvider.complete(); expect(tester.takeException(), isNull); }); testWidgets('do not crash when previous imageProvider completes after Switch is disposed', ( WidgetTester tester, ) async { final imageProvider1 = DelayedImageProvider(image); final imageProvider2 = DelayedImageProvider(image); Future buildSwitch(ImageProvider imageProvider) { return tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: Center( child: Switch(value: true, onChanged: null, inactiveThumbImage: imageProvider), ), ), ), ); } await buildSwitch(imageProvider1); expect(find.byType(Switch), findsOneWidget); // Replace the ImageProvider. await buildSwitch(imageProvider2); expect(find.byType(Switch), findsOneWidget); // Dispose the switch by taking down the tree. await tester.pumpWidget(Container()); expect(find.byType(Switch), findsNothing); // Completing the replaced ImageProvider shouldn't crash. imageProvider1.complete(); expect(tester.takeException(), isNull); imageProvider2.complete(); expect(tester.takeException(), isNull); }); }); group('Switch M3 only tests', () { testWidgets('M3 Switch has a 300-millisecond animation in total', (WidgetTester tester) async { final theme = ThemeData(); var value = false; await tester.pumpWidget( MaterialApp( theme: theme, home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, isFalse); final Rect switchRect = tester.getRect(find.byType(Switch)); final TestGesture gesture = await tester.startGesture(switchRect.centerLeft); await tester.pump(); await gesture.up(); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // M2 animation duration expect(tester.hasRunningAnimations, true); await tester.pump(const Duration(milliseconds: 101)); expect(tester.hasRunningAnimations, false); }); testWidgets('M3 Switch has a stadium shape in the middle of the track', ( WidgetTester tester, ) async { final theme = ThemeData(colorSchemeSeed: Colors.deepPurple); var value = false; await tester.pumpWidget( MaterialApp( theme: theme, home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, isFalse); final Rect switchRect = tester.getRect(find.byType(Switch)); final TestGesture gesture = await tester.startGesture(switchRect.centerLeft); await tester.pump(); await gesture.up(); await tester.pump(); // After 33 milliseconds, the switch thumb moves to the middle // and has a stadium shape with a size of (34x22). await tester.pump(const Duration(milliseconds: 33)); expect(tester.hasRunningAnimations, true); await expectLater(find.byType(Switch), matchesGoldenFile('switch_test.m3.transition.png')); }); testWidgets('M3 Switch thumb bounces in the end of the animation', (WidgetTester tester) async { final theme = ThemeData(); var value = false; await tester.pumpWidget( MaterialApp( theme: theme, home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( value: value, onChanged: (bool newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, isFalse); final Rect switchRect = tester.getRect(find.byType(Switch)); final TestGesture gesture = await tester.startGesture(switchRect.centerLeft); await tester.pump(); await gesture.up(); await tester.pump(); // The value on y axis is greater than 1 when t > 0.375 // 300 * 0.375 = 112.5 await tester.pump(const Duration(milliseconds: 113)); final ToggleableStateMixin state = tester.state( find.descendant( of: find.byType(Switch), matching: find.byWidgetPredicate( (Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch', ), ), ); expect(tester.hasRunningAnimations, true); expect(state.position.value, greaterThan(1)); }); testWidgets('Switch thumb shows correct pressed color - M3', (WidgetTester tester) async { final themeData = ThemeData(); final ColorScheme colors = themeData.colorScheme; Widget buildApp({bool enabled = true, bool value = true}) { return MaterialApp( theme: themeData, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Switch( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.primary, // track color style: PaintingStyle.fill, ) ..rrect( color: Colors.transparent, // track outline color style: PaintingStyle.stroke, ) ..rrect( color: colors.primaryContainer, rrect: RRect.fromLTRBR(26.0, 10.0, 54.0, 38.0, const Radius.circular(14.0)), ), ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildApp(value: false)); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.surfaceContainerHighest, // track color style: PaintingStyle.fill, ) ..rrect( color: colors.outline, // track outline color style: PaintingStyle.stroke, ) ..rrect(color: colors.onSurfaceVariant), ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildApp(enabled: false)); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.onSurface.withOpacity(0.12), // track color style: PaintingStyle.fill, ) ..rrect( color: Colors.transparent, // track outline color style: PaintingStyle.stroke, ) ..rrect(color: colors.surface.withOpacity(1.0)), ); await tester.pumpWidget(Container()); await tester.pumpWidget(buildApp(enabled: false, value: false)); await tester.press(find.byType(Switch)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: colors.surfaceContainerHighest.withOpacity(0.12), // track color style: PaintingStyle.fill, ) ..rrect( color: colors.onSurface.withOpacity(0.12), // track outline color style: PaintingStyle.stroke, ) ..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), ); }, variant: TargetPlatformVariant.mobile()); testWidgets('Track outline color resolves in active/enabled states', ( WidgetTester tester, ) async { const activeEnabledTrackOutlineColor = Color(0xFF000001); const activeDisabledTrackOutlineColor = Color(0xFF000002); const inactiveEnabledTrackOutlineColor = Color(0xFF000003); const inactiveDisabledTrackOutlineColor = Color(0xFF000004); Color getTrackOutlineColor(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledTrackOutlineColor; } return inactiveDisabledTrackOutlineColor; } if (states.contains(WidgetState.selected)) { return activeEnabledTrackOutlineColor; } return inactiveEnabledTrackOutlineColor; } final WidgetStateProperty trackOutlineColor = WidgetStateColor.resolveWith( getTrackOutlineColor, ); Widget buildSwitch({required bool enabled, required bool active}) { return Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( trackOutlineColor: trackOutlineColor, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(color: inactiveDisabledTrackOutlineColor, style: PaintingStyle.stroke), reason: 'Inactive disabled switch track outline should use this value', ); await tester.pumpWidget(buildSwitch(enabled: false, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(color: activeDisabledTrackOutlineColor, style: PaintingStyle.stroke), reason: 'Active disabled switch track outline should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(color: inactiveEnabledTrackOutlineColor), reason: 'Inactive enabled switch track outline should match these colors', ); await tester.pumpWidget(buildSwitch(enabled: true, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(color: activeEnabledTrackOutlineColor), reason: 'Active enabled switch track outline should match these colors', ); }); testWidgets('Switch track outline color resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredTrackOutlineColor = Color(0xFF000001); const focusedTrackOutlineColor = Color(0xFF000002); Color getTrackOutlineColor(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredTrackOutlineColor; } if (states.contains(WidgetState.focused)) { return focusedTrackOutlineColor; } return Colors.transparent; } final WidgetStateProperty trackOutlineColor = WidgetStateColor.resolveWith( getTrackOutlineColor, ); Widget buildSwitch() { return Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Switch( focusNode: focusNode, autofocus: true, value: true, trackOutlineColor: trackOutlineColor, onChanged: (_) {}, ), ), ), ); } await tester.pumpWidget(buildSwitch()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(color: focusedTrackOutlineColor, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline should match this color', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(color: hoveredTrackOutlineColor, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline should match this color', ); focusNode.dispose(); }); testWidgets('Track outline width resolves in active/enabled states', ( WidgetTester tester, ) async { const activeEnabledTrackOutlineWidth = 1.0; const activeDisabledTrackOutlineWidth = 2.0; const inactiveEnabledTrackOutlineWidth = 3.0; const inactiveDisabledTrackOutlineWidth = 4.0; double getTrackOutlineWidth(Set states) { if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.selected)) { return activeDisabledTrackOutlineWidth; } return inactiveDisabledTrackOutlineWidth; } if (states.contains(WidgetState.selected)) { return activeEnabledTrackOutlineWidth; } return inactiveEnabledTrackOutlineWidth; } final WidgetStateProperty trackOutlineWidth = WidgetStateProperty.resolveWith( getTrackOutlineWidth, ); Widget buildSwitch({required bool enabled, required bool active}) { return MaterialApp( home: Material( child: Center( child: Switch( trackOutlineWidth: trackOutlineWidth, value: active, onChanged: enabled ? (_) {} : null, ), ), ), ); } await tester.pumpWidget(buildSwitch(enabled: false, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(strokeWidth: inactiveDisabledTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Inactive disabled switch track outline width should be 4.0', ); await tester.pumpWidget(buildSwitch(enabled: false, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(strokeWidth: activeDisabledTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Active disabled switch track outline width should be 2.0', ); await tester.pumpWidget(buildSwitch(enabled: true, active: false)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(strokeWidth: inactiveEnabledTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Inactive enabled switch track outline width should be 3.0', ); await tester.pumpWidget(buildSwitch(enabled: true, active: true)); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(strokeWidth: activeEnabledTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline width should be 1.0', ); }); testWidgets('Switch track outline width resolves in hovered/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const hoveredTrackOutlineWidth = 4.0; const focusedTrackOutlineWidth = 6.0; double getTrackOutlineWidth(Set states) { if (states.contains(WidgetState.hovered)) { return hoveredTrackOutlineWidth; } if (states.contains(WidgetState.focused)) { return focusedTrackOutlineWidth; } return 8.0; } final WidgetStateProperty trackOutlineWidth = WidgetStateProperty.resolveWith( getTrackOutlineWidth, ); Widget buildSwitch() { return MaterialApp( home: Material( child: Center( child: Switch( focusNode: focusNode, autofocus: true, value: true, trackOutlineWidth: trackOutlineWidth, onChanged: (_) {}, ), ), ), ); } await tester.pumpWidget(buildSwitch()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(strokeWidth: focusedTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline width should be 6.0', ); // Start hovering final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); addTearDown(gesture.removePointer); await gesture.moveTo(tester.getCenter(find.byType(Switch))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill) ..rrect(strokeWidth: hoveredTrackOutlineWidth, style: PaintingStyle.stroke), reason: 'Active enabled switch track outline width should be 4.0', ); focusNode.dispose(); }); testWidgets('Switch can set icon - M3', (WidgetTester tester) async { final themeData = ThemeData( colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light, ); WidgetStateProperty thumbIcon(Icon? activeIcon, Icon? inactiveIcon) { return WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.selected)) { return activeIcon; } return inactiveIcon; }); } Widget buildSwitch({ required bool enabled, required bool active, Icon? activeIcon, Icon? inactiveIcon, }) { return Theme( data: themeData, child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Switch( thumbIcon: thumbIcon(activeIcon, inactiveIcon), value: active, onChanged: enabled ? (_) {} : null, ), ), ), ), ); } // active icon shows when switch is on. await tester.pumpWidget( buildSwitch(enabled: true, active: true, activeIcon: const Icon(Icons.close)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..rrect() ..paragraph(offset: const Offset(32.0, 16.0)), ); // inactive icon shows when switch is off. await tester.pumpWidget( buildSwitch(enabled: true, active: false, inactiveIcon: const Icon(Icons.close)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..rrect() ..rrect() ..paragraph(offset: const Offset(12.0, 16.0)), ); // active icon doesn't show when switch is off. await tester.pumpWidget( buildSwitch(enabled: true, active: false, activeIcon: const Icon(Icons.check)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..rrect() ..rrect(), ); // inactive icon doesn't show when switch is on. await tester.pumpWidget( buildSwitch(enabled: true, active: true, inactiveIcon: const Icon(Icons.check)), ); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..rrect() ..restore(), ); // without icon await tester.pumpWidget(buildSwitch(enabled: true, active: false)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect() ..rrect() ..rrect() ..restore(), ); }); }); testWidgets('Switch.adaptive(Cupertino) is focusable and has correct focus color', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Switch.adaptive'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; const focusColor = Color(0xffff0000); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Material( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Center( child: Switch.adaptive( value: value, onChanged: enabled ? (bool newValue) { setState(() { value = newValue; }); } : null, focusColor: focusColor, focusNode: focusNode, autofocus: true, ), ); }, ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( find.byType(Switch), paints ..rrect(color: const Color(0xff34c759)) ..rrect(color: const Color(0x00000000)) ..rrect(color: focusColor) ..clipRRect() ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) ..rrect(color: const Color(0xffffffff)), ); // Check the false value. value = false; await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( find.byType(Switch), paints ..rrect(color: const Color(0x28787880)) ..rrect(color: const Color(0x00000000)) ..rrect(color: focusColor) ..clipRRect() ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) ..rrect(color: const Color(0xffffffff)), ); // Check what happens when disabled. value = false; await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( find.byType(Switch), paints ..rrect(color: const Color(0x28787880)) ..clipRRect() ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) ..rrect(color: const Color(0xffffffff)), ); }); testWidgets('Switch.onFocusChange callback', (WidgetTester tester) async { final focusNode = FocusNode(debugLabel: 'Switch'); var focused = false; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: Switch( value: true, focusNode: focusNode, onFocusChange: (bool value) { focused = value; }, onChanged: (bool newValue) {}, ), ), ), ), ); focusNode.requestFocus(); await tester.pump(); expect(focused, isTrue); expect(focusNode.hasFocus, isTrue); focusNode.unfocus(); await tester.pump(); expect(focused, isFalse); expect(focusNode.hasFocus, isFalse); focusNode.dispose(); }); testWidgets('Switch.padding is respected', (WidgetTester tester) async { Widget buildSwitch({EdgeInsets? padding}) { return MaterialApp( home: Material( child: Center( child: Switch(padding: padding, value: true, onChanged: (_) {}), ), ), ); } await tester.pumpWidget(buildSwitch()); expect(tester.getSize(find.byType(Switch)), const Size(60.0, 48.0)); await tester.pumpWidget(buildSwitch(padding: EdgeInsets.zero)); expect(tester.getSize(find.byType(Switch)), const Size(52.0, 48.0)); await tester.pumpWidget(buildSwitch(padding: const EdgeInsets.all(4.0))); expect(tester.getSize(find.byType(Switch)), const Size(60.0, 56.0)); }); testWidgets('Material2 - Switch activeThumbColor', (WidgetTester tester) async { const activeColor = Color(0xffff0000); const activeThumbColor = Color(0xff00ff00); const activeTrackColor = Color(0xff0000ff); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (_) {}, activeColor: activeColor, activeThumbColor: activeThumbColor, activeTrackColor: activeTrackColor, ), ), ); }, ), ), ), ); expect(tester.widget(find.byType(Switch)).activeThumbColor, activeThumbColor); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( color: activeTrackColor, rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)), ) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: activeThumbColor), ); }); testWidgets('Material3 - Switch activeThumbColor', (WidgetTester tester) async { const activeColor = Color(0xffff0000); const activeThumbColor = Color(0xff00ff00); const activeTrackColor = Color(0xff0000ff); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (_) {}, activeColor: activeColor, activeThumbColor: activeThumbColor, activeTrackColor: activeTrackColor, ), ), ); }, ), ), ), ); expect(tester.widget(find.byType(Switch)).activeThumbColor, activeThumbColor); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect( style: PaintingStyle.fill, color: activeTrackColor, rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)), ) ..rrect() ..rrect(color: activeThumbColor), ); }); testWidgets('Material2 - Switch.adaptive activeThumbColor', (WidgetTester tester) async { const activeColor = Color(0xffff0000); const activeThumbColor = Color(0xff00ff00); const activeTrackColor = Color(0xff0000ff); Widget buildFrame(TargetPlatform platform) { return MaterialApp( key: UniqueKey(), theme: ThemeData(platform: platform, useMaterial3: false), home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch.adaptive( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (_) {}, activeColor: activeColor, activeThumbColor: activeThumbColor, activeTrackColor: activeTrackColor, ), ), ); }, ), ), ); } for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { final trackRRect = platform == TargetPlatform.iOS ? RRect.fromLTRBR(4.0, 8.5, 55.0, 39.5, const Radius.circular(15.5)) : RRect.fromLTRBR(4.0, 4.5, 55.0, 35.5, const Radius.circular(15.5)); await tester.pumpWidget(buildFrame(platform)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: activeTrackColor, rrect: trackRRect) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) ..rrect(color: activeThumbColor), ); } for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { final trackRRect = platform == TargetPlatform.fuchsia || platform == TargetPlatform.android ? RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)) : RRect.fromLTRBR(13.0, 13.0, 46.0, 27.0, const Radius.circular(7.0)); await tester.pumpWidget(buildFrame(platform)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: activeTrackColor, rrect: trackRRect) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x1f000000)) ..rrect(color: activeThumbColor), ); } }); testWidgets('Material3 - Switch.adaptive activeThumbColor', (WidgetTester tester) async { const activeColor = Color(0xffff0000); const activeThumbColor = Color(0xff00ff00); const activeTrackColor = Color(0xff0000ff); Widget buildFrame(TargetPlatform platform) { return MaterialApp( key: UniqueKey(), theme: ThemeData(platform: platform), home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Switch.adaptive( dragStartBehavior: DragStartBehavior.down, value: true, onChanged: (_) {}, activeColor: activeColor, activeThumbColor: activeThumbColor, activeTrackColor: activeTrackColor, ), ), ); }, ), ), ); } for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { final trackRRect = platform == TargetPlatform.iOS ? RRect.fromLTRBR(4.5, 8.5, 55.5, 39.5, const Radius.circular(15.5)) : RRect.fromLTRBR(4.5, 4.5, 55.5, 35.5, const Radius.circular(15.5)); await tester.pumpWidget(buildFrame(platform)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(color: activeTrackColor, rrect: trackRRect) ..rrect(color: const Color(0x00000000)) ..rrect(color: const Color(0x26000000)) ..rrect(color: const Color(0x0f000000)) ..rrect(color: const Color(0x0a000000)) ..rrect(color: activeThumbColor), ); } for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { final trackRRect = platform == TargetPlatform.fuchsia || platform == TargetPlatform.android ? RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)) : RRect.fromLTRBR(4.0, 4.0, 56.0, 36.0, const Radius.circular(16.0)); await tester.pumpWidget(buildFrame(platform)); expect( Material.of(tester.element(find.byType(Switch))), paints ..rrect(style: PaintingStyle.fill, color: activeTrackColor, rrect: trackRRect) ..rrect() ..rrect(color: activeThumbColor), ); } }); } class DelayedImageProvider extends ImageProvider { DelayedImageProvider(this.image); final ui.Image image; final Completer _completer = Completer(); @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } @override ImageStreamCompleter loadImage(DelayedImageProvider key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter(_completer.future); } Future complete() async { _completer.complete(ImageInfo(image: image)); } @override String toString() => '${describeIdentity(this)}()'; } class _TestImageProvider extends ImageProvider { _TestImageProvider({ImageStreamCompleter? streamCompleter}) { _streamCompleter = streamCompleter ?? OneFrameImageStreamCompleter(_completer.future); } final Completer _completer = Completer(); late ImageStreamCompleter _streamCompleter; bool get loadCalled => _loadCallCount > 0; int get loadCallCount => _loadCallCount; int _loadCallCount = 0; @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture<_TestImageProvider>(this); } @override void resolveStreamForKey( ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError, ) { super.resolveStreamForKey(configuration, stream, key, handleError); } @override ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) { _loadCallCount += 1; return _streamCompleter; } void complete(ui.Image image) { _completer.complete(ImageInfo(image: image)); } void fail(Object exception, StackTrace? stackTrace) { _completer.completeError(exception, stackTrace); } @override String toString() => '${describeIdentity(this)}()'; } Widget buildAdaptiveSwitch({ required TargetPlatform platform, bool enabled = true, bool value = true, bool autofocus = false, FocusNode? focusNode, Color? focusColor, SwitchThemeData? overallSwitchThemeData, SwitchThemeData? switchThemeData, Adaptation? switchThemeAdaptation, }) { final Widget adaptiveSwitch = Switch.adaptive( focusNode: focusNode, autofocus: autofocus, focusColor: focusColor, value: value, onChanged: enabled ? (_) {} : null, ); return MaterialApp( theme: ThemeData( platform: platform, switchTheme: overallSwitchThemeData, adaptations: switchThemeAdaptation == null ? null : >[switchThemeAdaptation], ), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: switchThemeData == null ? adaptiveSwitch : SwitchTheme(data: switchThemeData, child: adaptiveSwitch), ), ); }, ), ); } class _SwitchThemeAdaptation extends Adaptation { const _SwitchThemeAdaptation(); @override SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) { switch (theme.platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.windows: case TargetPlatform.linux: return defaultValue; case TargetPlatform.iOS: case TargetPlatform.macOS: return const SwitchThemeData( thumbColor: MaterialStatePropertyAll(Colors.lightGreen), trackColor: MaterialStatePropertyAll(Colors.deepPurple), ); } } }