mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
parent
f7fb14ec58
commit
59c9d4e845
|
@ -0,0 +1,84 @@
|
|||
// 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.
|
||||
|
||||
// Flutter code sample for [ExpansionTile] and [ExpansionTileController]
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ExpansionTileControllerApp());
|
||||
}
|
||||
|
||||
class ExpansionTileControllerApp extends StatefulWidget {
|
||||
const ExpansionTileControllerApp({ super.key });
|
||||
|
||||
@override
|
||||
State<ExpansionTileControllerApp> createState() => _ExpansionTileControllerAppState();
|
||||
}
|
||||
|
||||
class _ExpansionTileControllerAppState extends State<ExpansionTileControllerApp> {
|
||||
final ExpansionTileController controller = ExpansionTileController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Code Sample for ExpansionTileController.',
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('ExpansionTileController Example')),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
// A controller has been provided to the ExpansionTile because it's
|
||||
// going to be accessed from a component that is not within the
|
||||
// tile's BuildContext.
|
||||
ExpansionTile(
|
||||
controller: controller,
|
||||
title: const Text('ExpansionTile with explicit controller.'),
|
||||
children: <Widget>[
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const Text('ExpansionTile Contents'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
child: const Text('Expand/Collapse the Tile Above'),
|
||||
onPressed: () {
|
||||
if (controller.isExpanded) {
|
||||
controller.collapse();
|
||||
} else {
|
||||
controller.expand();
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
// A controller has not been provided to the ExpansionTile because
|
||||
// the automatically created one can be retrieved via the tile's BuildContext.
|
||||
ExpansionTile(
|
||||
title: const Text('ExpansionTile with implicit controller.'),
|
||||
children: <Widget>[
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
alignment: Alignment.center,
|
||||
child: ElevatedButton(
|
||||
child: const Text('Collapse This Tile'),
|
||||
onPressed: () {
|
||||
return ExpansionTileController.of(context).collapse();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// 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_api_samples/material/expansion_tile/expansion_tile.1.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Test the basics of ExpansionTileControllerApp', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.ExpansionTileControllerApp(),
|
||||
);
|
||||
|
||||
expect(find.text('ExpansionTile Contents'), findsNothing);
|
||||
expect(find.text('Collapse This Tile'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Expand/Collapse the Tile Above'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('ExpansionTile Contents'), findsOneWidget);
|
||||
await tester.tap(find.text('Expand/Collapse the Tile Above'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('ExpansionTile Contents'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('ExpansionTile with implicit controller.'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Collapse This Tile'), findsOneWidget);
|
||||
await tester.tap(find.text('Collapse This Tile'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Collapse This Tile'), findsNothing);
|
||||
});
|
||||
}
|
|
@ -15,6 +15,170 @@ import 'theme.dart';
|
|||
|
||||
const Duration _kExpand = Duration(milliseconds: 200);
|
||||
|
||||
/// Enables control over a single [ExpansionTile]'s expanded/collapsed state.
|
||||
///
|
||||
/// It can be useful to expand or collapse an [ExpansionTile]
|
||||
/// programatically, for example to reconfigure an existing expansion
|
||||
/// tile based on a system event. To do so, create an [ExpansionTile]
|
||||
/// with an [ExpansionTileController] that's owned by a stateful widget
|
||||
/// or look up the tile's automatically created [ExpansionTileController]
|
||||
/// with [ExpansionTileController.of]
|
||||
///
|
||||
/// The controller's [expand] and [collapse] methods cause the
|
||||
/// the [ExpansionTile] to rebuild, so they may not be called from
|
||||
/// a build method.
|
||||
class ExpansionTileController {
|
||||
/// Create a controller to be used with [ExpansionTile.controller].
|
||||
ExpansionTileController();
|
||||
|
||||
_ExpansionTileState? _state;
|
||||
|
||||
/// Whether the [ExpansionTile] built with this controller is in expanded state.
|
||||
///
|
||||
/// This property doesn't take the animation into account. It reports `true`
|
||||
/// even if the expansion animation is not completed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [expand], which expands the [ExpansionTile].
|
||||
/// * [collapse], which collapses the [ExpansionTile].
|
||||
/// * [ExpansionTile.controller] to create an ExpansionTile with a controller.
|
||||
bool get isExpanded {
|
||||
assert(_state != null);
|
||||
return _state!._isExpanded;
|
||||
}
|
||||
|
||||
/// Expands the [ExpansionTile] that was built with this controller;
|
||||
///
|
||||
/// Normally the tile is expanded automatically when the user taps on the header.
|
||||
/// It is sometimes useful to trigger the expansion programmatically due
|
||||
/// to external changes.
|
||||
///
|
||||
/// If the tile is already in the expanded state (see [isExpanded]), calling
|
||||
/// this method has no effect.
|
||||
///
|
||||
/// Calling this method may cause the [ExpansionTile] to rebuild, so it may
|
||||
/// not be called from a build method.
|
||||
///
|
||||
/// Calling this method will trigger an [ExpansionTile.onExpansionChanged] callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [collapse], which collapses the tile.
|
||||
/// * [isExpanded] to check whether the tile is expanded.
|
||||
/// * [ExpansionTile.controller] to create an ExpansionTile with a controller.
|
||||
void expand() {
|
||||
assert(_state != null);
|
||||
if (!isExpanded) {
|
||||
_state!._toggleExpansion();
|
||||
}
|
||||
}
|
||||
|
||||
/// Collapses the [ExpansionTile] that was built with this controller.
|
||||
///
|
||||
/// Normally the tile is collapsed automatically when the user taps on the header.
|
||||
/// It can be useful sometimes to trigger the collapse programmatically due
|
||||
/// to some external changes.
|
||||
///
|
||||
/// If the tile is already in the collapsed state (see [isExpanded]), calling
|
||||
/// this method has no effect.
|
||||
///
|
||||
/// Calling this method may cause the [ExpansionTile] to rebuild, so it may
|
||||
/// not be called from a build method.
|
||||
///
|
||||
/// Calling this method will trigger an [ExpansionTile.onExpansionChanged] callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [expand], which expands the tile.
|
||||
/// * [isExpanded] to check whether the tile is expanded.
|
||||
/// * [ExpansionTile.controller] to create an ExpansionTile with a controller.
|
||||
void collapse() {
|
||||
assert(_state != null);
|
||||
if (isExpanded) {
|
||||
_state!._toggleExpansion();
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the [ExpansionTileController] for the closest [ExpansionTile] instance
|
||||
/// that encloses the given context.
|
||||
///
|
||||
/// If no [ExpansionTile] encloses the given context, calling this
|
||||
/// method will cause an assert in debug mode, and throw an
|
||||
/// exception in release mode.
|
||||
///
|
||||
/// To return null if there is no [ExpansionTile] use [maybeOf] instead.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Typical usage of the [ExpansionTileController.of] function is to call it from within the
|
||||
/// `build` method of a descendant of an [ExpansionTile].
|
||||
///
|
||||
/// When the [ExpansionTile] is actually created in the same `build`
|
||||
/// function as the callback that refers to the controller, then the
|
||||
/// `context` argument to the `build` function can't be used to find
|
||||
/// the [ExpansionTileController] (since it's "above" the widget
|
||||
/// being returned in the widget tree). In cases like that you can
|
||||
/// add a [Builder] widget, which provides a new scope with a
|
||||
/// [BuildContext] that is "under" the [ExpansionTile]:
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// A more efficient solution is to split your build function into
|
||||
/// several widgets. This introduces a new context from which you
|
||||
/// can obtain the [ExpansionTileController]. With this approach you
|
||||
/// would have an outer widget that creates the [ExpansionTile]
|
||||
/// populated by instances of your new inner widgets, and then in
|
||||
/// these inner widgets you would use [ExpansionTileController.of].
|
||||
static ExpansionTileController of(BuildContext context) {
|
||||
final _ExpansionTileState? result = context.findAncestorStateOfType<_ExpansionTileState>();
|
||||
if (result != null) {
|
||||
return result._tileController;
|
||||
}
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'ExpansionTileController.of() called with a context that does not contain a ExpansionTile.',
|
||||
),
|
||||
ErrorDescription(
|
||||
'No ExpansionTile ancestor could be found starting from the context that was passed to ExpansionTileController.of(). '
|
||||
'This usually happens when the context provided is from the same StatefulWidget as that '
|
||||
'whose build function actually creates the ExpansionTile widget being sought.',
|
||||
),
|
||||
ErrorHint(
|
||||
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
|
||||
'context that is "under" the ExpansionTile. For an example of this, please see the '
|
||||
'documentation for ExpansionTileController.of():\n'
|
||||
' https://api.flutter.dev/flutter/material/ExpansionTile/of.html',
|
||||
),
|
||||
ErrorHint(
|
||||
'A more efficient solution is to split your build function into several widgets. This '
|
||||
'introduces a new context from which you can obtain the ExpansionTile. In this solution, '
|
||||
'you would have an outer widget that creates the ExpansionTile populated by instances of '
|
||||
'your new inner widgets, and then in these inner widgets you would use ExpansionTileController.of().\n'
|
||||
'An other solution is assign a GlobalKey to the ExpansionTile, '
|
||||
'then use the key.currentState property to obtain the ExpansionTile rather than '
|
||||
'using the ExpansionTileController.of() function.',
|
||||
),
|
||||
context.describeElement('The context used was'),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Finds the [ExpansionTile] from the closest instance of this class that
|
||||
/// encloses the given context and returns its [ExpansionTileController].
|
||||
///
|
||||
/// If no [ExpansionTile] encloses the given context then return null.
|
||||
/// To throw an exception instead, use [of] instead of this function.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [of], a similar function to this one that throws if no [ExpansionTile]
|
||||
/// encloses the given context. Also includes some sample code in its
|
||||
/// documentation.
|
||||
static ExpansionTileController? maybeOf(BuildContext context) {
|
||||
return context.findAncestorStateOfType<_ExpansionTileState>()?._tileController;
|
||||
}
|
||||
}
|
||||
|
||||
/// A single-line [ListTile] with an expansion arrow icon that expands or collapses
|
||||
/// the tile to reveal or hide the [children].
|
||||
///
|
||||
|
@ -40,6 +204,13 @@ const Duration _kExpand = Duration(milliseconds: 200);
|
|||
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example demonstrates how an [ExpansionTileController] can be used to
|
||||
/// programatically expand or collapse an [ExpansionTile].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListTile], useful for creating expansion tile [children] when the
|
||||
|
@ -74,6 +245,7 @@ class ExpansionTile extends StatefulWidget {
|
|||
this.collapsedShape,
|
||||
this.clipBehavior,
|
||||
this.controlAffinity,
|
||||
this.controller,
|
||||
}) : assert(
|
||||
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
|
||||
'CrossAxisAlignment.baseline is not supported since the expanded children '
|
||||
|
@ -310,6 +482,13 @@ class ExpansionTile extends StatefulWidget {
|
|||
/// which means that the expansion arrow icon will appear on the tile's trailing edge.
|
||||
final ListTileControlAffinity? controlAffinity;
|
||||
|
||||
/// If provided, the controller can be used to expand and collapse tiles.
|
||||
///
|
||||
/// In cases were control over the tile's state is needed from a callback triggered
|
||||
/// by a widget within the tile, [ExpansionTileController.of] may be more convenient
|
||||
/// than supplying a controller.
|
||||
final ExpansionTileController? controller;
|
||||
|
||||
@override
|
||||
State<ExpansionTile> createState() => _ExpansionTileState();
|
||||
}
|
||||
|
@ -324,7 +503,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||
final ColorTween _iconColorTween = ColorTween();
|
||||
final ColorTween _backgroundColorTween = ColorTween();
|
||||
|
||||
late AnimationController _controller;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _iconTurns;
|
||||
late Animation<double> _heightFactor;
|
||||
late Animation<ShapeBorder?> _border;
|
||||
|
@ -333,37 +512,43 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||
late Animation<Color?> _backgroundColor;
|
||||
|
||||
bool _isExpanded = false;
|
||||
late ExpansionTileController _tileController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(duration: _kExpand, vsync: this);
|
||||
_heightFactor = _controller.drive(_easeInTween);
|
||||
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
|
||||
_border = _controller.drive(_borderTween.chain(_easeOutTween));
|
||||
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
|
||||
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
|
||||
_backgroundColor = _controller.drive(_backgroundColorTween.chain(_easeOutTween));
|
||||
_animationController = AnimationController(duration: _kExpand, vsync: this);
|
||||
_heightFactor = _animationController.drive(_easeInTween);
|
||||
_iconTurns = _animationController.drive(_halfTween.chain(_easeInTween));
|
||||
_border = _animationController.drive(_borderTween.chain(_easeOutTween));
|
||||
_headerColor = _animationController.drive(_headerColorTween.chain(_easeInTween));
|
||||
_iconColor = _animationController.drive(_iconColorTween.chain(_easeInTween));
|
||||
_backgroundColor = _animationController.drive(_backgroundColorTween.chain(_easeOutTween));
|
||||
|
||||
_isExpanded = PageStorage.maybeOf(context)?.readState(context) as bool? ?? widget.initiallyExpanded;
|
||||
if (_isExpanded) {
|
||||
_controller.value = 1.0;
|
||||
_animationController.value = 1.0;
|
||||
}
|
||||
|
||||
assert(widget.controller?._state == null);
|
||||
_tileController = widget.controller ?? ExpansionTileController();
|
||||
_tileController._state = this;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_tileController._state = null;
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
void _toggleExpansion() {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
if (_isExpanded) {
|
||||
_controller.forward();
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_controller.reverse().then<void>((void value) {
|
||||
_animationController.reverse().then<void>((void value) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -377,6 +562,10 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||
widget.onExpansionChanged?.call(_isExpanded);
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
_toggleExpansion();
|
||||
}
|
||||
|
||||
// Platform or null affinity defaults to trailing.
|
||||
ListTileControlAffinity _effectiveAffinity(ListTileControlAffinity? affinity) {
|
||||
switch (affinity ?? ListTileControlAffinity.trailing) {
|
||||
|
@ -491,7 +680,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context);
|
||||
final bool closed = !_isExpanded && _controller.isDismissed;
|
||||
final bool closed = !_isExpanded && _animationController.isDismissed;
|
||||
final bool shouldRemoveChildren = closed && !widget.maintainState;
|
||||
|
||||
final Widget result = Offstage(
|
||||
|
@ -509,7 +698,7 @@ class _ExpansionTileState extends State<ExpansionTile> with SingleTickerProvider
|
|||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
animation: _animationController.view,
|
||||
builder: _buildChildren,
|
||||
child: shouldRemoveChildren ? null : result,
|
||||
);
|
||||
|
|
|
@ -729,4 +729,119 @@ void main() {
|
|||
expect(getTextColor(), theme.colorScheme.primary);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('ExpansionTileController isExpanded, expand() and collapse()', (WidgetTester tester) async {
|
||||
final ExpansionTileController controller = ExpansionTileController();
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: ExpansionTile(
|
||||
controller: controller,
|
||||
title: const Text('Title'),
|
||||
children: const <Widget>[
|
||||
Text('Child 0'),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('Child 0'), findsNothing);
|
||||
expect(controller.isExpanded, isFalse);
|
||||
controller.expand();
|
||||
expect(controller.isExpanded, isTrue);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Child 0'), findsOneWidget);
|
||||
expect(controller.isExpanded, isTrue);
|
||||
controller.collapse();
|
||||
expect(controller.isExpanded, isFalse);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Child 0'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Calling ExpansionTileController.expand/collapsed has no effect if it is already expanded/collapsed', (WidgetTester tester) async {
|
||||
final ExpansionTileController controller = ExpansionTileController();
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: ExpansionTile(
|
||||
controller: controller,
|
||||
title: const Text('Title'),
|
||||
initiallyExpanded: true,
|
||||
children: const <Widget>[
|
||||
Text('Child 0'),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('Child 0'), findsOneWidget);
|
||||
expect(controller.isExpanded, isTrue);
|
||||
controller.expand();
|
||||
expect(controller.isExpanded, isTrue);
|
||||
await tester.pump();
|
||||
expect(tester.hasRunningAnimations, isFalse);
|
||||
expect(find.text('Child 0'), findsOneWidget);
|
||||
controller.collapse();
|
||||
expect(controller.isExpanded, isFalse);
|
||||
await tester.pump();
|
||||
expect(tester.hasRunningAnimations, isTrue);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.isExpanded, isFalse);
|
||||
expect(find.text('Child 0'), findsNothing);
|
||||
controller.collapse();
|
||||
expect(controller.isExpanded, isFalse);
|
||||
await tester.pump();
|
||||
expect(tester.hasRunningAnimations, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Call to ExpansionTileController.of()', (WidgetTester tester) async {
|
||||
final GlobalKey titleKey = GlobalKey();
|
||||
final GlobalKey childKey = GlobalKey();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
title: Text('Title', key: titleKey),
|
||||
children: <Widget>[
|
||||
Text('Child 0', key: childKey),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final ExpansionTileController controller1 = ExpansionTileController.of(childKey.currentContext!);
|
||||
expect(controller1.isExpanded, isTrue);
|
||||
|
||||
final ExpansionTileController controller2 = ExpansionTileController.of(titleKey.currentContext!);
|
||||
expect(controller2.isExpanded, isTrue);
|
||||
|
||||
expect(controller1, controller2);
|
||||
});
|
||||
|
||||
testWidgets('Call to ExpansionTile.maybeOf()', (WidgetTester tester) async {
|
||||
final GlobalKey titleKey = GlobalKey();
|
||||
final GlobalKey nonDescendantKey = GlobalKey();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ExpansionTile(
|
||||
title: Text('Title', key: titleKey),
|
||||
children: const <Widget>[
|
||||
Text('Child 0'),
|
||||
],
|
||||
),
|
||||
Text('Non descendant', key: nonDescendantKey),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
final ExpansionTileController? controller1 = ExpansionTileController.maybeOf(titleKey.currentContext!);
|
||||
expect(controller1, isNotNull);
|
||||
expect(controller1?.isExpanded, isFalse);
|
||||
|
||||
final ExpansionTileController? controller2 = ExpansionTileController.maybeOf(nonDescendantKey.currentContext!);
|
||||
expect(controller2, isNull);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue