// 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/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { testWidgetsWithLeakTracking('Semantics can merge sibling group', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); const SemanticsTag third = SemanticsTag('3'); ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) { expect(configs.length, 3); final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder(); final List<SemanticsConfiguration> sibling = <SemanticsConfiguration>[]; // Merge first and third for (final SemanticsConfiguration config in configs) { if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) { sibling.add(config); } else { builder.markAsMergeUp(config); } } builder.markAsSiblingMergeGroup(sibling); return builder.build(); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Semantics( label: 'parent', child: TestConfigDelegate( delegate: delegate, child: Column( children: <Widget>[ Semantics( label: '1', tagForChildren: first, child: const SizedBox(width: 100, height: 100), // this tests that empty nodes disappear ), Semantics( label: '2', tagForChildren: second, child: const SizedBox(width: 100, height: 100), ), Semantics( label: '3', tagForChildren: third, child: const SizedBox(width: 100, height: 100), ), ], ), ), ), ), ); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( label: 'parent\n2', ), TestSemantics.rootChild( label: '1\n3', ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true)); semantics.dispose(); }); testWidgetsWithLeakTracking('Semantics can drop semantics config', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); const SemanticsTag third = SemanticsTag('3'); ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) { final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder(); // Merge first and third for (final SemanticsConfiguration config in configs) { if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) { continue; } builder.markAsMergeUp(config); } return builder.build(); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Semantics( label: 'parent', child: TestConfigDelegate( delegate: delegate, child: Column( children: <Widget>[ Semantics( label: '1', tagForChildren: first, child: const SizedBox(width: 100, height: 100), // this tests that empty nodes disappear ), Semantics( label: '2', tagForChildren: second, child: const SizedBox(width: 100, height: 100), ), Semantics( label: '3', tagForChildren: third, child: const SizedBox(width: 100, height: 100), ), ], ), ), ), ), ); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( label: 'parent\n2', ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true)); semantics.dispose(); }); testWidgetsWithLeakTracking('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async { const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); const SemanticsTag third = SemanticsTag('3'); ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) { final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder(); // Marks the same one twice. builder.markAsMergeUp(configs.first); builder.markAsMergeUp(configs.first); return builder.build(); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Semantics( label: 'parent', child: TestConfigDelegate( delegate: delegate, child: Column( children: <Widget>[ Semantics( label: '1', tagForChildren: first, child: const SizedBox(width: 100, height: 100), // this tests that empty nodes disappear ), Semantics( label: '2', tagForChildren: second, child: const SizedBox(width: 100, height: 100), ), Semantics( label: '3', tagForChildren: third, child: const SizedBox(width: 100, height: 100), ), ], ), ), ), ), ); expect(tester.takeException(), isAssertionError); }); testWidgetsWithLeakTracking('Semantics throws when mark the same config twice case 2', (WidgetTester tester) async { const SemanticsTag first = SemanticsTag('1'); const SemanticsTag second = SemanticsTag('2'); const SemanticsTag third = SemanticsTag('3'); ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) { final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder(); // Marks the same one twice. builder.markAsMergeUp(configs.first); builder.markAsSiblingMergeGroup(<SemanticsConfiguration>[configs.first]); return builder.build(); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Semantics( label: 'parent', child: TestConfigDelegate( delegate: delegate, child: Column( children: <Widget>[ Semantics( label: '1', tagForChildren: first, child: const SizedBox(width: 100, height: 100), // this tests that empty nodes disappear ), Semantics( label: '2', tagForChildren: second, child: const SizedBox(width: 100, height: 100), ), Semantics( label: '3', tagForChildren: third, child: const SizedBox(width: 100, height: 100), ), ], ), ), ), ), ); expect(tester.takeException(), isAssertionError); }); testWidgetsWithLeakTracking('RenderObject with semantics child delegate will mark correct boundary dirty', (WidgetTester tester) async { final UniqueKey inner = UniqueKey(); final UniqueKey boundaryParent = UniqueKey(); final UniqueKey grandBoundaryParent = UniqueKey(); ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) { final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder(); configs.forEach(builder.markAsMergeUp); return builder.build(); } await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MarkSemanticsDirtySpy( key: grandBoundaryParent, child: MarkSemanticsDirtySpy( key: boundaryParent, child: TestConfigDelegate( delegate: delegate, child: Column( children: <Widget>[ Semantics( label: 'label', child: MarkSemanticsDirtySpy( key: inner, child: const Text('inner'), ), ), ], ), ), ), ), ), ); final RenderMarkSemanticsDirtySpy innerObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(inner)); final RenderTestConfigDelegate objectWithDelegate = tester.renderObject<RenderTestConfigDelegate>(find.byType(TestConfigDelegate)); final RenderMarkSemanticsDirtySpy boundaryParentObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(boundaryParent)); final RenderMarkSemanticsDirtySpy grandBoundaryParentObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(grandBoundaryParent)); void resetBuildState() { innerObject.hasRebuildSemantics = false; boundaryParentObject.hasRebuildSemantics = false; grandBoundaryParentObject.hasRebuildSemantics = false; } // Sanity check expect(innerObject.hasRebuildSemantics, isTrue); expect(boundaryParentObject.hasRebuildSemantics, isTrue); expect(grandBoundaryParentObject.hasRebuildSemantics, isTrue); resetBuildState(); innerObject.markNeedsSemanticsUpdate(); await tester.pump(); // Inner boundary should not trigger rebuild above it. expect(innerObject.hasRebuildSemantics, isTrue); expect(boundaryParentObject.hasRebuildSemantics, isFalse); expect(grandBoundaryParentObject.hasRebuildSemantics, isFalse); resetBuildState(); objectWithDelegate.markNeedsSemanticsUpdate(); await tester.pump(); // object with delegate rebuilds up to grand parent boundary; expect(innerObject.hasRebuildSemantics, isTrue); expect(boundaryParentObject.hasRebuildSemantics, isTrue); expect(grandBoundaryParentObject.hasRebuildSemantics, isTrue); resetBuildState(); boundaryParentObject.markNeedsSemanticsUpdate(); await tester.pump(); // Render objects in between child delegate and grand boundary parent does // not mark the grand boundary parent dirty because it should not change the // generated sibling nodes. expect(innerObject.hasRebuildSemantics, isTrue); expect(boundaryParentObject.hasRebuildSemantics, isTrue); expect(grandBoundaryParentObject.hasRebuildSemantics, isFalse); }); } class MarkSemanticsDirtySpy extends SingleChildRenderObjectWidget { const MarkSemanticsDirtySpy({super.key, super.child}); @override RenderMarkSemanticsDirtySpy createRenderObject(BuildContext context) => RenderMarkSemanticsDirtySpy(); } class RenderMarkSemanticsDirtySpy extends RenderProxyBox { RenderMarkSemanticsDirtySpy(); bool hasRebuildSemantics = false; @override void describeSemanticsConfiguration(SemanticsConfiguration config) { config.isSemanticBoundary = true; } @override void assembleSemanticsNode( SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children, ) { hasRebuildSemantics = true; } } class TestConfigDelegate extends SingleChildRenderObjectWidget { const TestConfigDelegate({super.key, required this.delegate, super.child}); final ChildSemanticsConfigurationsDelegate delegate; @override RenderTestConfigDelegate createRenderObject(BuildContext context) => RenderTestConfigDelegate( delegate: delegate, ); @override void updateRenderObject(BuildContext context, RenderTestConfigDelegate renderObject) { renderObject.delegate = delegate; } } class RenderTestConfigDelegate extends RenderProxyBox { RenderTestConfigDelegate({ ChildSemanticsConfigurationsDelegate? delegate, }) : _delegate = delegate; ChildSemanticsConfigurationsDelegate? get delegate => _delegate; ChildSemanticsConfigurationsDelegate? _delegate; set delegate(ChildSemanticsConfigurationsDelegate? value) { if (value != _delegate) { markNeedsSemanticsUpdate(); } _delegate = value; } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { config.childConfigurationsDelegate = delegate; } }