// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('SemanticsDebugger will schedule a frame', (WidgetTester tester) async { await tester.pumpWidget(SemanticsDebugger(child: Container())); expect(tester.binding.hasScheduledFrame, isTrue); }); testWidgets('SemanticsDebugger smoke test', (WidgetTester tester) async { // This is a smoketest to verify that adding a debugger doesn't crash. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Stack( children: [ Semantics(), Semantics(container: true), Semantics(label: 'label', textDirection: TextDirection.ltr), ], ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Stack( children: [ Semantics(), Semantics(container: true), Semantics(label: 'label', textDirection: TextDirection.ltr), ], ), ), ), ); expect(true, isTrue); // expect that we reach here without crashing }); testWidgets('SemanticsDebugger draw persistent color based on structure', ( WidgetTester tester, ) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Stack( children: [Semantics(container: true, child: Semantics(container: true))], ), ), ), ); expect( find.byType(SemanticsDebugger), paints ..rect() ..rect(color: const Color(0xFFF866FF)), ); }); testWidgets('SemanticsDebugger reparents subtree', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Stack( children: [ Semantics(label: 'label1', textDirection: TextDirection.ltr), Positioned( key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0, child: Semantics(label: 'label2', textDirection: TextDirection.ltr), ), ], ), ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Stack( children: [ Semantics(label: 'label1', textDirection: TextDirection.ltr), Semantics( container: true, child: Stack( children: [ Positioned( key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0, child: Semantics(label: 'label2', textDirection: TextDirection.ltr), ), Semantics(label: 'label3', textDirection: TextDirection.ltr), ], ), ), ], ), ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Stack( children: [ Semantics(label: 'label1', textDirection: TextDirection.ltr), Semantics( container: true, child: Stack( children: [ Positioned( key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0, child: Semantics(label: 'label2', textDirection: TextDirection.ltr), ), Semantics(label: 'label3', textDirection: TextDirection.ltr), Semantics(label: 'label4', textDirection: TextDirection.ltr), ], ), ), ], ), ), ), ); expect(tester.takeException(), isNull); }); testWidgets('SemanticsDebugger interaction test', (WidgetTester tester) async { final log = []; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Material( child: ListView( children: [ ElevatedButton( onPressed: () { log.add('top'); }, child: const Text('TOP'), ), ElevatedButton( onPressed: () { log.add('bottom'); }, child: const Text('BOTTOM'), ), ], ), ), ), ), ); await tester.tap(find.text('TOP'), warnIfMissed: false); // hitting the debugger expect(log, equals(['top'])); log.clear(); await tester.tap(find.text('BOTTOM'), warnIfMissed: false); // hitting the debugger expect(log, equals(['bottom'])); log.clear(); }); testWidgets('SemanticsDebugger interaction test - negative', (WidgetTester tester) async { final log = []; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Material( child: ListView( children: [ ElevatedButton( onPressed: () { log.add('top'); }, child: const Text('TOP', textDirection: TextDirection.ltr), ), ExcludeSemantics( child: ElevatedButton( onPressed: () { log.add('bottom'); }, child: const Text('BOTTOM', textDirection: TextDirection.ltr), ), ), ], ), ), ), ), ); await tester.tap(find.text('TOP'), warnIfMissed: false); // hitting the debugger expect(log, equals(['top'])); log.clear(); await tester.tap(find.text('BOTTOM'), warnIfMissed: false); // hitting the debugger expect(log, equals([])); log.clear(); }); testWidgets('SemanticsDebugger scroll test', (WidgetTester tester) async { final Key childKey = UniqueKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: ListView( children: [Container(key: childKey, height: 5000.0, color: Colors.green[500])], ), ), ), ); expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); await tester.fling( find.byType(ListView), const Offset(0.0, -200.0), 200.0, warnIfMissed: false, ); // hitting the debugger); await tester.pump(); expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-480.0)); await tester.fling( find.byType(ListView), const Offset(200.0, 0.0), 200.0, warnIfMissed: false, ); // hitting the debugger); await tester.pump(); expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-480.0)); await tester.fling( find.byType(ListView), const Offset(-200.0, 0.0), 200.0, warnIfMissed: false, ); // hitting the debugger); await tester.pump(); expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-480.0)); await tester.fling( find.byType(ListView), const Offset(0.0, 200.0), 200.0, warnIfMissed: false, ); // hitting the debugger); await tester.pump(); expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0)); }); testWidgets('SemanticsDebugger long press', (WidgetTester tester) async { var didLongPress = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: GestureDetector( onLongPress: () { expect(didLongPress, isFalse); didLongPress = true; }, child: const Text('target', textDirection: TextDirection.ltr), ), ), ), ); await tester.longPress(find.text('target'), warnIfMissed: false); // hitting the debugger expect(didLongPress, isTrue); }); testWidgets('SemanticsDebugger slider', (WidgetTester tester) async { var value = 0.75; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: MediaQueryData.fromView(tester.view), child: Material( child: Center( child: Slider( value: value, onChanged: (double newValue) { value = newValue; }, ), ), ), ), ), ), ), ), ); // The fling below must be such that the velocity estimation examines an // offset greater than the kTouchSlop. Too slow or too short a distance, and // it won't trigger. The actual distance moved doesn't matter since this is // interpreted as a gesture by the semantics debugger and sent to the widget // as a semantic action that always moves by 10% of the complete track. await tester.fling( find.byType(Slider), const Offset(-100.0, 0.0), 2000.0, warnIfMissed: false, ); // hitting the debugger switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: expect(value, equals(0.65)); case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.android: case TargetPlatform.fuchsia: expect(value, equals(0.70)); } }, variant: TargetPlatformVariant.all()); testWidgets('SemanticsDebugger checkbox', (WidgetTester tester) async { final Key keyTop = UniqueKey(); final Key keyBottom = UniqueKey(); bool? valueTop = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( child: Material( child: ListView( children: [ Checkbox( key: keyTop, value: valueTop, onChanged: (bool? newValue) { valueTop = newValue; }, ), Checkbox(key: keyBottom, value: false, onChanged: null), ], ), ), ), ), ); await tester.tap(find.byKey(keyTop), warnIfMissed: false); // hitting the debugger expect(valueTop, isTrue); valueTop = false; expect(valueTop, isFalse); await tester.tap(find.byKey(keyBottom), warnIfMissed: false); // hitting the debugger expect(valueTop, isFalse); }); testWidgets('SemanticsDebugger checkbox message', (WidgetTester tester) async { final Key checkbox = UniqueKey(); final Key checkboxUnchecked = UniqueKey(); final Key checkboxDisabled = UniqueKey(); final Key checkboxDisabledUnchecked = UniqueKey(); final Key debugger = UniqueKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( key: debugger, child: Material( child: ListView( children: [ Semantics( container: true, key: checkbox, child: Checkbox(value: true, onChanged: (bool? _) {}), ), Semantics( container: true, key: checkboxUnchecked, child: Checkbox(value: false, onChanged: (bool? _) {}), ), Semantics( container: true, key: checkboxDisabled, child: const Checkbox(value: true, onChanged: null), ), Semantics( container: true, key: checkboxDisabledUnchecked, child: const Checkbox(value: false, onChanged: null), ), ], ), ), ), ), ); expect( _getMessageShownInSemanticsDebugger( widgetKey: checkbox, debuggerKey: debugger, tester: tester, ), 'checked', ); expect( _getMessageShownInSemanticsDebugger( widgetKey: checkboxUnchecked, debuggerKey: debugger, tester: tester, ), 'unchecked', ); expect( _getMessageShownInSemanticsDebugger( widgetKey: checkboxDisabled, debuggerKey: debugger, tester: tester, ), 'checked; disabled', ); expect( _getMessageShownInSemanticsDebugger( widgetKey: checkboxDisabledUnchecked, debuggerKey: debugger, tester: tester, ), 'unchecked; disabled', ); }); testWidgets( 'SemanticsDebugger ignores duplicated label and tooltip for Android', (WidgetTester tester) async { final Key child = UniqueKey(); final Key debugger = UniqueKey(); final isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( key: debugger, child: Material( child: Semantics(container: true, key: child, label: 'text', tooltip: 'text'), ), ), ), ); expect( _getMessageShownInSemanticsDebugger( widgetKey: child, debuggerKey: debugger, tester: tester, ), isPlatformAndroid ? 'text' : 'text\ntext', ); }, variant: TargetPlatformVariant.all(), ); testWidgets('SemanticsDebugger textfield', (WidgetTester tester) async { final textField = UniqueKey(); final debugger = UniqueKey(); await tester.pumpWidget( MaterialApp( home: SemanticsDebugger( key: debugger, child: Material(child: TextField(key: textField)), ), ), ); final dynamic semanticsDebuggerPainter = _getSemanticsDebuggerPainter( debuggerKey: debugger, tester: tester, ); final RenderObject renderTextfield = tester.renderObject( find.descendant(of: find.byKey(textField), matching: find.byType(Semantics)).first, ); expect( // ignore: avoid_dynamic_calls semanticsDebuggerPainter.getMessage(renderTextfield.debugSemantics), 'textfield', ); }); testWidgets('SemanticsDebugger label style is used in the painter.', (WidgetTester tester) async { final debugger = UniqueKey(); const labelStyle = TextStyle(color: Colors.amber); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SemanticsDebugger( key: debugger, labelStyle: labelStyle, child: Semantics(label: 'label', textDirection: TextDirection.ltr), ), ), ); expect( // ignore: avoid_dynamic_calls _getSemanticsDebuggerPainter(debuggerKey: debugger, tester: tester).labelStyle, labelStyle, ); }); testWidgets('SemanticsDebugger label for rtl.', (WidgetTester tester) async { final debugger = UniqueKey(); final Key label = UniqueKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: SemanticsDebugger( key: debugger, child: Semantics(label: 'ملصق', textDirection: TextDirection.rtl, key: label), ), ), ); expect( _getMessageShownInSemanticsDebugger(widgetKey: label, debuggerKey: debugger, tester: tester), '\u2067ملصق\u2069', ); }); testWidgets('SemanticsDebugger turns on semantics.', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/147665. expect(tester.binding.semanticsEnabled, isFalse); await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: SemanticsDebugger(child: Semantics(label: 'Hello World')), ), ); expect(tester.binding.semanticsEnabled, isTrue); }, semanticsEnabled: false); } String _getMessageShownInSemanticsDebugger({ required Key widgetKey, required Key debuggerKey, required WidgetTester tester, }) { final dynamic semanticsDebuggerPainter = _getSemanticsDebuggerPainter( debuggerKey: debuggerKey, tester: tester, ); // ignore: avoid_dynamic_calls return semanticsDebuggerPainter.getMessage( tester.renderObject(find.byKey(widgetKey)).debugSemantics, ) as String; } dynamic _getSemanticsDebuggerPainter({required Key debuggerKey, required WidgetTester tester}) { final customPaint = tester .widgetList( find.descendant(of: find.byKey(debuggerKey), matching: find.byType(CustomPaint)), ) .first as CustomPaint; final dynamic semanticsDebuggerPainter = customPaint.foregroundPainter; expect(semanticsDebuggerPainter.runtimeType.toString(), '_SemanticsDebuggerPainter'); return semanticsDebuggerPainter; }