mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
729 lines
24 KiB
Dart
729 lines
24 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
import 'multi_view_testing.dart';
|
|
|
|
void main() {
|
|
testWidgets('Widgets running with runApp can find View', (WidgetTester tester) async {
|
|
FlutterView? viewOf;
|
|
FlutterView? viewMaybeOf;
|
|
|
|
runApp(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
viewOf = View.of(context);
|
|
viewMaybeOf = View.maybeOf(context);
|
|
return Container();
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(viewOf, isNotNull);
|
|
expect(viewOf, isA<FlutterView>());
|
|
expect(viewMaybeOf, isNotNull);
|
|
expect(viewMaybeOf, isA<FlutterView>());
|
|
});
|
|
|
|
testWidgets('Widgets running with pumpWidget can find View', (WidgetTester tester) async {
|
|
FlutterView? view;
|
|
FlutterView? viewMaybeOf;
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
view = View.of(context);
|
|
viewMaybeOf = View.maybeOf(context);
|
|
return Container();
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(view, isNotNull);
|
|
expect(view, isA<FlutterView>());
|
|
expect(viewMaybeOf, isNotNull);
|
|
expect(viewMaybeOf, isA<FlutterView>());
|
|
});
|
|
|
|
testWidgets('cannot find View behind a LookupBoundary', (WidgetTester tester) async {
|
|
await tester.pumpWidget(LookupBoundary(child: Container()));
|
|
|
|
final BuildContext context = tester.element(find.byType(Container));
|
|
|
|
expect(View.maybeOf(context), isNull);
|
|
expect(
|
|
() => View.of(context),
|
|
throwsA(
|
|
isA<FlutterError>().having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains(
|
|
'The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('child of view finds view, parentPipelineOwner, mediaQuery', (
|
|
WidgetTester tester,
|
|
) async {
|
|
FlutterView? outsideView;
|
|
FlutterView? insideView;
|
|
PipelineOwner? outsideParent;
|
|
PipelineOwner? insideParent;
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
outsideView = View.maybeOf(context);
|
|
outsideParent = View.pipelineOwnerOf(context);
|
|
return View(
|
|
view: tester.view,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
insideView = View.maybeOf(context);
|
|
insideParent = View.pipelineOwnerOf(context);
|
|
return const SizedBox();
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
expect(outsideView, isNull);
|
|
expect(insideView, equals(tester.view));
|
|
|
|
expect(outsideParent, isNotNull);
|
|
expect(insideParent, isNotNull);
|
|
expect(outsideParent, isNot(equals(insideParent)));
|
|
|
|
expect(outsideParent, tester.binding.rootPipelineOwner);
|
|
expect(insideParent, equals(tester.renderObject(find.byType(SizedBox)).owner));
|
|
|
|
final pipelineOwners = <PipelineOwner>[];
|
|
tester.binding.rootPipelineOwner.visitChildren((PipelineOwner child) {
|
|
pipelineOwners.add(child);
|
|
});
|
|
expect(pipelineOwners.single, equals(insideParent));
|
|
});
|
|
|
|
testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(view: tester.view, child: const SizedBox()),
|
|
View(view: tester.view, child: const SizedBox()),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError e) => e.message,
|
|
'message',
|
|
contains('Multiple widgets used the same GlobalKey'),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('ViewCollection may start with zero views', (WidgetTester tester) async {
|
|
expect(() => const ViewCollection(views: <Widget>[]), returnsNormally);
|
|
});
|
|
|
|
testWidgets('ViewAnchor.child does not see surrounding view', (WidgetTester tester) async {
|
|
FlutterView? inside;
|
|
FlutterView? outside;
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
outside = View.maybeOf(context);
|
|
return ViewAnchor(
|
|
view: Builder(
|
|
builder: (BuildContext context) {
|
|
inside = View.maybeOf(context);
|
|
return View(view: FakeView(tester.view), child: const SizedBox());
|
|
},
|
|
),
|
|
child: const SizedBox(),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
expect(inside, isNull);
|
|
expect(outside, isNotNull);
|
|
});
|
|
|
|
testWidgets('ViewAnchor layout order', (WidgetTester tester) async {
|
|
Finder findSpyWidget(int label) {
|
|
return find.byWidgetPredicate((Widget w) => w is SpyRenderWidget && w.label == label);
|
|
}
|
|
|
|
final log = <String>[];
|
|
await tester.pumpWidget(
|
|
SpyRenderWidget(
|
|
label: 1,
|
|
log: log,
|
|
child: ViewAnchor(
|
|
view: View(
|
|
view: FakeView(tester.view),
|
|
child: SpyRenderWidget(label: 2, log: log),
|
|
),
|
|
child: SpyRenderWidget(label: 3, log: log),
|
|
),
|
|
),
|
|
);
|
|
log.clear();
|
|
tester.renderObject(findSpyWidget(3)).markNeedsLayout();
|
|
tester.renderObject(findSpyWidget(2)).markNeedsLayout();
|
|
tester.renderObject(findSpyWidget(1)).markNeedsLayout();
|
|
await tester.pump();
|
|
expect(log, <String>['layout 1', 'layout 3', 'layout 2']);
|
|
});
|
|
|
|
testWidgets('visitChildren of ViewAnchor visits both children', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
ViewAnchor(
|
|
view: View(
|
|
view: FakeView(tester.view),
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
child: const SizedBox(),
|
|
),
|
|
);
|
|
final Element viewAnchorElement = tester.element(
|
|
find.byElementPredicate(
|
|
(Element e) => e.runtimeType.toString() == '_MultiChildComponentElement',
|
|
),
|
|
);
|
|
final children = <Element>[];
|
|
viewAnchorElement.visitChildren((Element element) {
|
|
children.add(element);
|
|
});
|
|
expect(children, hasLength(2));
|
|
|
|
await tester.pumpWidget(const ViewAnchor(child: SizedBox()));
|
|
children.clear();
|
|
viewAnchorElement.visitChildren((Element element) {
|
|
children.add(element);
|
|
});
|
|
expect(children, hasLength(1));
|
|
});
|
|
|
|
testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(view: tester.view, child: const SizedBox()),
|
|
View(view: FakeView(tester.view), child: const SizedBox()),
|
|
View(view: FakeView(tester.view, viewId: 423), child: const SizedBox()),
|
|
],
|
|
),
|
|
);
|
|
final Element viewAnchorElement = tester.element(
|
|
find.byElementPredicate(
|
|
(Element e) => e.runtimeType.toString() == '_MultiChildComponentElement',
|
|
),
|
|
);
|
|
final children = <Element>[];
|
|
viewAnchorElement.visitChildren((Element element) {
|
|
children.add(element);
|
|
});
|
|
expect(children, hasLength(3));
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[View(view: tester.view, child: const SizedBox())],
|
|
),
|
|
);
|
|
children.clear();
|
|
viewAnchorElement.visitChildren((Element element) {
|
|
children.add(element);
|
|
});
|
|
expect(children, hasLength(1));
|
|
});
|
|
|
|
group('renderObject getter', () {
|
|
testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async {
|
|
late BuildContext builderContext;
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
builderContext = context;
|
|
return View(view: tester.view, child: const SizedBox());
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderObject? renderObject = builderContext.findRenderObject();
|
|
expect(renderObject, isNotNull);
|
|
expect(renderObject, isA<RenderView>());
|
|
expect(renderObject, tester.renderObject(find.byType(View)));
|
|
expect(tester.element(find.byType(Builder)).renderObject, renderObject);
|
|
});
|
|
|
|
testWidgets('ancestors of ViewCollection get null for renderObject', (
|
|
WidgetTester tester,
|
|
) async {
|
|
late BuildContext builderContext;
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
builderContext = context;
|
|
return ViewCollection(
|
|
views: <Widget>[
|
|
View(view: tester.view, child: const SizedBox()),
|
|
View(view: FakeView(tester.view), child: const SizedBox()),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderObject? renderObject = builderContext.findRenderObject();
|
|
expect(renderObject, isNull);
|
|
expect(tester.element(find.byType(Builder)).renderObject, isNull);
|
|
});
|
|
|
|
testWidgets('ancestors of a ViewAnchor see the right RenderObject', (
|
|
WidgetTester tester,
|
|
) async {
|
|
late BuildContext builderContext;
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
builderContext = context;
|
|
return ViewAnchor(
|
|
view: View(
|
|
view: FakeView(tester.view),
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
child: const SizedBox(),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
final RenderObject? renderObject = builderContext.findRenderObject();
|
|
expect(renderObject, isNotNull);
|
|
expect(renderObject, isA<RenderConstrainedBox>());
|
|
expect(renderObject, tester.renderObject(find.byType(SizedBox)));
|
|
expect(tester.element(find.byType(Builder)).renderObject, renderObject);
|
|
});
|
|
});
|
|
|
|
testWidgets(
|
|
'correctly switches between view configurations',
|
|
experimentalLeakTesting: LeakTesting.settings
|
|
.withIgnoredAll(), // Leaking by design as contains deprecated items.
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
View(
|
|
view: tester.view,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
|
child: const SizedBox(),
|
|
),
|
|
);
|
|
RenderObject renderView = tester.renderObject(find.byType(View));
|
|
expect(renderView, same(tester.binding.renderView));
|
|
expect(renderView.owner, same(tester.binding.pipelineOwner));
|
|
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
View(view: tester.view, child: const SizedBox()),
|
|
);
|
|
renderView = tester.renderObject(find.byType(View));
|
|
expect(renderView, isNot(same(tester.binding.renderView)));
|
|
expect(renderView.owner, isNot(same(tester.binding.pipelineOwner)));
|
|
expect(
|
|
tester.renderObject(find.byType(SizedBox)).owner,
|
|
isNot(same(tester.binding.pipelineOwner)),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
View(
|
|
view: tester.view,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
|
child: const SizedBox(),
|
|
),
|
|
);
|
|
renderView = tester.renderObject(find.byType(View));
|
|
expect(renderView, same(tester.binding.renderView));
|
|
expect(renderView.owner, same(tester.binding.pipelineOwner));
|
|
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
|
|
|
|
expect(
|
|
() => View(
|
|
view: tester.view,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
|
child: const SizedBox(),
|
|
),
|
|
throwsAssertionError,
|
|
);
|
|
expect(
|
|
() => View(
|
|
view: tester.view,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
|
child: const SizedBox(),
|
|
),
|
|
throwsAssertionError,
|
|
);
|
|
expect(
|
|
() => View(
|
|
view: FakeView(tester.view),
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
|
child: const SizedBox(),
|
|
),
|
|
throwsAssertionError,
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('attaches itself correctly', (WidgetTester tester) async {
|
|
final Key viewKey = UniqueKey();
|
|
late final PipelineOwner parentPipelineOwner;
|
|
await tester.pumpWidget(
|
|
ViewAnchor(
|
|
view: Builder(
|
|
builder: (BuildContext context) {
|
|
parentPipelineOwner = View.pipelineOwnerOf(context);
|
|
return View(key: viewKey, view: FakeView(tester.view), child: const SizedBox());
|
|
},
|
|
),
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
);
|
|
|
|
expect(parentPipelineOwner, isNot(RendererBinding.instance.rootPipelineOwner));
|
|
|
|
final RenderView rawView = tester.renderObject<RenderView>(find.byKey(viewKey));
|
|
expect(RendererBinding.instance.renderViews, contains(rawView));
|
|
|
|
final children = <PipelineOwner>[];
|
|
parentPipelineOwner.visitChildren((PipelineOwner child) {
|
|
children.add(child);
|
|
});
|
|
final PipelineOwner rawViewOwner = rawView.owner!;
|
|
expect(children, contains(rawViewOwner));
|
|
|
|
// Remove that View from the tree.
|
|
await tester.pumpWidget(const ViewAnchor(child: ColoredBox(color: Colors.green)));
|
|
|
|
expect(rawView.owner, isNull);
|
|
expect(RendererBinding.instance.renderViews, isNot(contains(rawView)));
|
|
children.clear();
|
|
parentPipelineOwner.visitChildren((PipelineOwner child) {
|
|
children.add(child);
|
|
});
|
|
expect(children, isNot(contains(rawViewOwner)));
|
|
});
|
|
|
|
testWidgets('RenderView does not use size of child if constraints are tight', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const physicalSize = Size(300, 600);
|
|
final Size logicalSize = physicalSize / tester.view.devicePixelRatio;
|
|
tester.view.physicalConstraints = ViewConstraints.tight(physicalSize);
|
|
await tester.pumpWidget(const Placeholder());
|
|
|
|
final RenderView renderView = tester.renderObject<RenderView>(find.byType(View));
|
|
expect(renderView.constraints, BoxConstraints.tight(logicalSize));
|
|
expect(renderView.size, logicalSize);
|
|
|
|
final RenderBox child = renderView.child!;
|
|
expect(child.constraints, BoxConstraints.tight(logicalSize));
|
|
expect(child.debugCanParentUseSize, isFalse);
|
|
expect(child.size, logicalSize);
|
|
});
|
|
|
|
testWidgets('RenderView sizes itself to child if constraints allow it (unconstrained)', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const size = Size(300, 600);
|
|
tester.view.physicalConstraints = const ViewConstraints(); // unconstrained
|
|
await tester.pumpWidget(SizedBox.fromSize(size: size));
|
|
|
|
final RenderView renderView = tester.renderObject<RenderView>(find.byType(View));
|
|
expect(renderView.constraints, const BoxConstraints());
|
|
expect(renderView.size, size);
|
|
|
|
final RenderBox child = renderView.child!;
|
|
expect(child.constraints, const BoxConstraints());
|
|
expect(child.debugCanParentUseSize, isTrue);
|
|
expect(child.size, size);
|
|
});
|
|
|
|
testWidgets('RenderView sizes itself to child if constraints allow it (constrained)', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const size = Size(30, 60);
|
|
const viewConstraints = ViewConstraints(maxWidth: 333, maxHeight: 666);
|
|
final boxConstraints = BoxConstraints.fromViewConstraints(
|
|
viewConstraints / tester.view.devicePixelRatio,
|
|
);
|
|
tester.view.physicalConstraints = viewConstraints;
|
|
await tester.pumpWidget(SizedBox.fromSize(size: size));
|
|
|
|
final RenderView renderView = tester.renderObject<RenderView>(find.byType(View));
|
|
expect(renderView.constraints, boxConstraints);
|
|
expect(renderView.size, size);
|
|
|
|
final RenderBox child = renderView.child!;
|
|
expect(child.constraints, boxConstraints);
|
|
expect(child.debugCanParentUseSize, isTrue);
|
|
expect(child.size, size);
|
|
});
|
|
|
|
testWidgets('RenderView respects constraints when child wants to be bigger than allowed', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const size = Size(3000, 6000);
|
|
const viewConstraints = ViewConstraints(maxWidth: 300, maxHeight: 600);
|
|
tester.view.physicalConstraints = viewConstraints;
|
|
await tester.pumpWidget(SizedBox.fromSize(size: size));
|
|
|
|
final RenderView renderView = tester.renderObject<RenderView>(find.byType(View));
|
|
expect(renderView.size, const Size(100, 200)); // viewConstraints.biggest / devicePixelRatio
|
|
|
|
final RenderBox child = renderView.child!;
|
|
expect(child.debugCanParentUseSize, isTrue);
|
|
expect(child.size, const Size(100, 200));
|
|
});
|
|
|
|
testWidgets('ViewFocusEvents cause unfocusing and refocusing', (WidgetTester tester) async {
|
|
late FlutterView view;
|
|
late FocusNode focusNode;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
view = View.of(context);
|
|
focusNode = Focus.of(context);
|
|
return Container();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
final unfocusEvent = ViewFocusEvent(
|
|
viewId: view.viewId,
|
|
state: ViewFocusState.unfocused,
|
|
direction: ViewFocusDirection.forward,
|
|
);
|
|
|
|
final focusEvent = ViewFocusEvent(
|
|
viewId: view.viewId,
|
|
state: ViewFocusState.focused,
|
|
direction: ViewFocusDirection.backward,
|
|
);
|
|
|
|
focusNode.requestFocus();
|
|
await tester.pump();
|
|
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isFalse);
|
|
|
|
ServicesBinding.instance.platformDispatcher.onViewFocusChange?.call(unfocusEvent);
|
|
await tester.pump();
|
|
|
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
|
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue);
|
|
|
|
ServicesBinding.instance.platformDispatcher.onViewFocusChange?.call(focusEvent);
|
|
await tester.pump();
|
|
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
expect(FocusManager.instance.rootScope.hasPrimaryFocus, isFalse);
|
|
});
|
|
|
|
testWidgets(
|
|
'View notifies engine that a view should have focus when a widget focus change occurs.',
|
|
(WidgetTester tester) async {
|
|
final nodeA = FocusNode(debugLabel: 'a');
|
|
addTearDown(nodeA.dispose);
|
|
|
|
FlutterView? view;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(focusNode: nodeA, child: const Text('a')),
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
view = View.of(context);
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
var notifyCount = 0;
|
|
void handleFocusChange() {
|
|
notifyCount++;
|
|
}
|
|
|
|
tester.binding.focusManager.addListener(handleFocusChange);
|
|
addTearDown(() => tester.binding.focusManager.removeListener(handleFocusChange));
|
|
tester.binding.platformDispatcher.resetFocusedViewTestValues();
|
|
|
|
nodeA.requestFocus();
|
|
await tester.pump();
|
|
final List<ViewFocusEvent> events = tester.binding.platformDispatcher.testFocusEvents;
|
|
expect(events.length, equals(1));
|
|
expect(events.last.viewId, equals(view?.viewId));
|
|
expect(events.last.direction, equals(ViewFocusDirection.forward));
|
|
expect(events.last.state, equals(ViewFocusState.focused));
|
|
expect(nodeA.hasPrimaryFocus, isTrue);
|
|
expect(notifyCount, equals(1));
|
|
notifyCount = 0;
|
|
},
|
|
);
|
|
|
|
testWidgets('Switching focus between views yields the correct events.', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final nodeA = FocusNode(debugLabel: 'a');
|
|
addTearDown(nodeA.dispose);
|
|
|
|
FlutterView? view;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(focusNode: nodeA, child: const Text('a')),
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
view = View.of(context);
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
var notifyCount = 0;
|
|
void handleFocusChange() {
|
|
notifyCount++;
|
|
}
|
|
|
|
tester.binding.focusManager.addListener(handleFocusChange);
|
|
addTearDown(() => tester.binding.focusManager.removeListener(handleFocusChange));
|
|
tester.binding.platformDispatcher.resetFocusedViewTestValues();
|
|
|
|
// Focus and make sure engine is notified.
|
|
nodeA.requestFocus();
|
|
await tester.pump();
|
|
List<ViewFocusEvent> events = tester.binding.platformDispatcher.testFocusEvents;
|
|
expect(events.length, equals(1));
|
|
expect(events.last.viewId, equals(view?.viewId));
|
|
expect(events.last.direction, equals(ViewFocusDirection.forward));
|
|
expect(events.last.state, equals(ViewFocusState.focused));
|
|
expect(nodeA.hasPrimaryFocus, isTrue);
|
|
expect(notifyCount, equals(1));
|
|
notifyCount = 0;
|
|
tester.binding.platformDispatcher.resetFocusedViewTestValues();
|
|
|
|
// Unfocus all views.
|
|
tester.binding.platformDispatcher.onViewFocusChange?.call(
|
|
ViewFocusEvent(
|
|
viewId: view!.viewId,
|
|
state: ViewFocusState.unfocused,
|
|
direction: ViewFocusDirection.forward,
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(nodeA.hasFocus, isFalse);
|
|
expect(tester.binding.platformDispatcher.testFocusEvents, isEmpty);
|
|
expect(notifyCount, equals(1));
|
|
notifyCount = 0;
|
|
tester.binding.platformDispatcher.resetFocusedViewTestValues();
|
|
|
|
// Focus another view.
|
|
tester.binding.platformDispatcher.onViewFocusChange?.call(
|
|
const ViewFocusEvent(
|
|
viewId: 100,
|
|
state: ViewFocusState.focused,
|
|
direction: ViewFocusDirection.forward,
|
|
),
|
|
);
|
|
|
|
// Focusing another view should unfocus this node without notifying the
|
|
// engine to unfocus.
|
|
await tester.pump();
|
|
expect(nodeA.hasFocus, isFalse);
|
|
expect(tester.binding.platformDispatcher.testFocusEvents, isEmpty);
|
|
expect(notifyCount, equals(0));
|
|
notifyCount = 0;
|
|
tester.binding.platformDispatcher.resetFocusedViewTestValues();
|
|
|
|
// Re-focusing the node should notify the engine that this view is focused.
|
|
nodeA.requestFocus();
|
|
await tester.pump();
|
|
expect(nodeA.hasPrimaryFocus, isTrue);
|
|
events = tester.binding.platformDispatcher.testFocusEvents;
|
|
expect(events.length, equals(1));
|
|
expect(events.last.viewId, equals(view?.viewId));
|
|
expect(events.last.direction, equals(ViewFocusDirection.forward));
|
|
expect(events.last.state, equals(ViewFocusState.focused));
|
|
expect(notifyCount, equals(1));
|
|
notifyCount = 0;
|
|
tester.binding.platformDispatcher.resetFocusedViewTestValues();
|
|
});
|
|
}
|
|
|
|
class SpyRenderWidget extends SizedBox {
|
|
const SpyRenderWidget({super.key, required this.label, required this.log, super.child});
|
|
|
|
final int label;
|
|
final List<String> log;
|
|
|
|
@override
|
|
RenderSpy createRenderObject(BuildContext context) {
|
|
return RenderSpy(additionalConstraints: const BoxConstraints(), label: label, log: log);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, RenderSpy renderObject) {
|
|
renderObject
|
|
..label = label
|
|
..log = log;
|
|
}
|
|
}
|
|
|
|
class RenderSpy extends RenderConstrainedBox {
|
|
RenderSpy({required super.additionalConstraints, required this.label, required this.log});
|
|
|
|
int label;
|
|
List<String> log;
|
|
|
|
@override
|
|
void performLayout() {
|
|
log.add('layout $label');
|
|
super.performLayout();
|
|
}
|
|
}
|