dropdown_test.dart 17.5 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math' as math;
6
import 'dart:ui' show window;
7

8 9 10
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

11 12
import '../widgets/semantics_tester.dart';

13 14 15 16 17 18
const List<String> menuItems = const <String>['one', 'two', 'three', 'four'];

final Type dropdownButtonType = new DropdownButton<String>(
  onChanged: (_){ },
  items: const <DropdownMenuItem<String>>[]
).runtimeType;
19

20 21 22 23 24 25
Widget buildFrame({
    Key buttonKey,
    String value: 'two',
    ValueChanged<String> onChanged,
    bool isDense: false,
    Widget hint,
26 27
    List<String> items: menuItems,
    FractionalOffset alignment: FractionalOffset.center,
28
  }) {
29 30
  return new MaterialApp(
    home: new Material(
31 32
      child: new Align(
        alignment: alignment,
33 34 35
        child: new DropdownButton<String>(
          key: buttonKey,
          value: value,
36
          hint: hint,
37 38
          onChanged: onChanged,
          isDense: isDense,
39
          items: items.map((String item) {
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
            return new DropdownMenuItem<String>(
              key: new ValueKey<String>(item),
              value: item,
              child: new Text(item, key: new ValueKey<String>(item + "Text")),
            );
          }).toList(),
        ),
      ),
    ),
  );
}

// When the dropdown's menu is popped up, a RenderParagraph for the selected
// menu's text item will appear both in the dropdown button and in the menu.
// The RenderParagraphs should be aligned, i.e. they should have the same
// size and location.
void checkSelectedItemTextGeometry(WidgetTester tester, String value) {
  final List<RenderBox> boxes = tester.renderObjectList(find.byKey(new ValueKey<String>(value + 'Text'))).toList();
  expect(boxes.length, equals(2));
  final RenderBox box0 = boxes[0];
  final RenderBox box1 = boxes[1];
61
  expect(box0.localToGlobal(Offset.zero), equals(box1.localToGlobal(Offset.zero)));
62 63 64 65
  expect(box0.size, equals(box1.size));
}

bool sameGeometry(RenderBox box1, RenderBox box2) {
66
  expect(box1.localToGlobal(Offset.zero), equals(box2.localToGlobal(Offset.zero)));
67 68 69 70
  expect(box1.size.height, equals(box2.size.height));
  return true;
}

71
void main() {
72
  testWidgets('Dropdown button control test', (WidgetTester tester) async {
73
    String value = 'one';
74 75 76 77
    void didChangeValue(String newValue) {
      value = newValue;
    }

78
    Widget build() => buildFrame(value: value, onChanged: didChangeValue);
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

    await tester.pumpWidget(build());

    await tester.tap(find.text('one'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('one'));

    await tester.tap(find.text('three').last);

    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('three'));

    await tester.tap(find.text('three'));
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('three'));

    await tester.pumpWidget(build());

    await tester.tap(find.text('two').last);

    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('two'));
  });

111
  testWidgets('Dropdown button with no app', (WidgetTester tester) async {
112
    String value = 'one';
113 114 115 116 117 118 119 120 121 122 123 124
    void didChangeValue(String newValue) {
      value = newValue;
    }

    Widget build() {
      return new Navigator(
        initialRoute: '/',
        onGenerateRoute: (RouteSettings settings) {
          return new MaterialPageRoute<Null>(
            settings: settings,
            builder: (BuildContext context) {
              return new Material(
125
                child: buildFrame(value: 'one', onChanged: didChangeValue),
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
              );
            },
          );
        }
      );
    }

    await tester.pumpWidget(build());

    await tester.tap(find.text('one'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('one'));

    await tester.tap(find.text('three').last);

    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('three'));

    await tester.tap(find.text('three'));
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('three'));

    await tester.pumpWidget(build());

    await tester.tap(find.text('two').last);

    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    expect(value, equals('two'));
  });

164
  testWidgets('Dropdown screen edges', (WidgetTester tester) async {
165
    int value = 4;
166
    final List<DropdownMenuItem<int>> items = <DropdownMenuItem<int>>[];
167
    for (int i = 0; i < 20; ++i)
168
      items.add(new DropdownMenuItem<int>(value: i, child: new Text('$i')));
169 170 171 172 173

    void handleChanged(int newValue) {
      value = newValue;
    }

174
    final DropdownButton<int> button = new DropdownButton<int>(
175 176
      value: value,
      onChanged: handleChanged,
177
      items: items,
178 179
    );

180
    await tester.pumpWidget(
181 182 183 184
      new MaterialApp(
        home: new Material(
          child: new Align(
            alignment: FractionalOffset.topCenter,
185 186 187 188
            child: button,
          ),
        ),
      ),
189 190
    );

191 192 193
    await tester.tap(find.text('4'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation
194

195 196
    // We should have two copies of item 5, one in the menu and one in the
    // button itself.
197
    expect(tester.elementList(find.text('5')), hasLength(2));
198 199 200

    // We should only have one copy of item 19, which is in the button itself.
    // The copy in the menu shouldn't be in the tree because it's off-screen.
201
    expect(tester.elementList(find.text('19')), hasLength(1));
202

203
    expect(value, 4);
204
    await tester.tap(find.byWidget(button));
205
    expect(value, 4);
206 207
    // this waits for the route's completer to complete, which calls handleChanged
    await tester.idle();
208
    expect(value, 4);
209 210 211

    // TODO(abarth): Remove these calls to pump once navigator cleans up its
    // pop transitions.
212 213
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation
214
  });
215

216
  testWidgets('Dropdown button aligns selected menu item', (WidgetTester tester) async {
217 218
    final Key buttonKey = new UniqueKey();
    final String value = 'two';
219 220 221 222

    Widget build() => buildFrame(buttonKey: buttonKey, value: value);

    await tester.pumpWidget(build());
223
    final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
224
    assert(buttonBox.attached);
225
    final Offset buttonOriginBeforeTap = buttonBox.localToGlobal(Offset.zero);
226 227 228 229 230 231

    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    // Tapping the dropdown button should not cause it to move.
232
    expect(buttonBox.localToGlobal(Offset.zero), equals(buttonOriginBeforeTap));
233 234 235 236

    // The selected dropdown item is both in menu we just popped up, and in
    // the IndexedStack contained by the dropdown button. Both of them should
    // have the same origin and height as the dropdown button.
237
    final List<RenderObject> itemBoxes = tester.renderObjectList(find.byKey(const ValueKey<String>('two'))).toList();
238 239 240
    expect(itemBoxes.length, equals(2));
    for(RenderBox itemBox in itemBoxes) {
      assert(itemBox.attached);
241
      expect(buttonBox.localToGlobal(Offset.zero), equals(itemBox.localToGlobal(Offset.zero)));
242 243 244 245 246 247 248 249
      expect(buttonBox.size.height, equals(itemBox.size.height));
    }

    // The two RenderParagraph objects, for the 'two' items' Text children,
    // should have the same size and location.
    checkSelectedItemTextGeometry(tester, 'two');
  });

250
  testWidgets('Dropdown button with isDense:true aligns selected menu item', (WidgetTester tester) async {
251 252
    final Key buttonKey = new UniqueKey();
    final String value = 'two';
253 254 255 256

    Widget build() => buildFrame(buttonKey: buttonKey, value: value, isDense: true);

    await tester.pumpWidget(build());
257
    final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
258 259 260 261 262 263 264 265 266
    assert(buttonBox.attached);

    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    // The selected dropdown item is both in menu we just popped up, and in
    // the IndexedStack contained by the dropdown button. Both of them should
    // have the same vertical center as the button.
267
    final List<RenderBox> itemBoxes = tester.renderObjectList(find.byKey(const ValueKey<String>('two'))).toList();
268 269 270 271
    expect(itemBoxes.length, equals(2));

    // When isDense is true, the button's height is reduced. The menu items'
    // heights are not.
272
    final double menuItemHeight = itemBoxes.map((RenderBox box) => box.size.height).reduce(math.max);
273 274 275 276
    expect(menuItemHeight, greaterThan(buttonBox.size.height));

    for(RenderBox itemBox in itemBoxes) {
      assert(itemBox.attached);
277 278 279
      final Offset buttonBoxCenter = buttonBox.size.center(buttonBox.localToGlobal(Offset.zero));
      final Offset itemBoxCenter =  itemBox.size.center(itemBox.localToGlobal(Offset.zero));
      expect(buttonBoxCenter.dy, equals(itemBoxCenter.dy));
280 281 282 283 284 285
    }

    // The two RenderParagraph objects, for the 'two' items' Text children,
    // should have the same size and location.
    checkSelectedItemTextGeometry(tester, 'two');
  });
286 287

  testWidgets('Size of DropdownButton with null value', (WidgetTester tester) async {
288
    final Key buttonKey = new UniqueKey();
289 290 291 292 293
    String value;

    Widget build() => buildFrame(buttonKey: buttonKey, value: value);

    await tester.pumpWidget(build());
294
    final RenderBox buttonBoxNullValue = tester.renderObject(find.byKey(buttonKey));
295 296 297 298 299
    assert(buttonBoxNullValue.attached);


    value = 'three';
    await tester.pumpWidget(build());
300
    final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
301 302
    assert(buttonBox.attached);

303
    // A Dropdown button with a null value should be the same size as a
304
    // one with a non-null value.
305
    expect(buttonBox.localToGlobal(Offset.zero), equals(buttonBoxNullValue.localToGlobal(Offset.zero)));
306 307 308 309
    expect(buttonBox.size, equals(buttonBoxNullValue.size));
  });

  testWidgets('Layout of a DropdownButton with null value', (WidgetTester tester) async {
310
    final Key buttonKey = new UniqueKey();
311 312 313 314 315 316 317 318 319
    String value;

    void onChanged(String newValue) {
      value = newValue;
    }

    Widget build() => buildFrame(buttonKey: buttonKey, value: value, onChanged: onChanged);

    await tester.pumpWidget(build());
320
    final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
    assert(buttonBox.attached);

    // Show the menu.
    await tester.tap(find.byKey(buttonKey));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    // Tap on item 'one', which must appear over the button.
    await tester.tap(find.byKey(buttonKey));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the menu animation

    await tester.pumpWidget(build());
    expect(value, equals('one'));
  });

337
  testWidgets('Size of DropdownButton with null value and a hint', (WidgetTester tester) async {
338
    final Key buttonKey = new UniqueKey();
339 340 341
    String value;

    // The hint will define the dropdown's width
342
    Widget build() => buildFrame(buttonKey: buttonKey, value: value, hint: const Text('onetwothree'));
343 344 345

    await tester.pumpWidget(build());
    expect(find.text('onetwothree'), findsOneWidget);
346
    final RenderBox buttonBoxHintValue = tester.renderObject(find.byKey(buttonKey));
347 348 349 350 351
    assert(buttonBoxHintValue.attached);


    value = 'three';
    await tester.pumpWidget(build());
352
    final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
353 354
    assert(buttonBox.attached);

355
    // A Dropdown button with a null value and a hint should be the same size as a
356
    // one with a non-null value.
357
    expect(buttonBox.localToGlobal(Offset.zero), equals(buttonBoxHintValue.localToGlobal(Offset.zero)));
358 359 360
    expect(buttonBox.size, equals(buttonBoxHintValue.size));
  });

361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
  testWidgets('Dropdown menus must fit within the screen', (WidgetTester tester) async {

    // The dropdown menu isn't readaily accessible. To find it we're assuming that it
    // contains a ListView and that it's an instance of _DropdownMenu.
    Rect getMenuRect() {
      Rect menuRect;
      tester.element(find.byType(ListView)).visitAncestorElements((Element element) {
        if (element.toString().startsWith("_DropdownMenu")) {
          final RenderBox box = element.findRenderObject();
          assert(box != null);
          menuRect =  box.localToGlobal(Offset.zero) & box.size;
          return false;
        }
        return true;
      });
      assert(menuRect != null);
      return menuRect;
    }

    // In all of the tests that follow we're assuming that the dropdown menu
    // is horizontally aligned with the center of the dropdown button and padded
    // on the top, left, and right.
    const EdgeInsets buttonPadding = const EdgeInsets.only(top: 8.0, left: 16.0, right: 24.0);

    Rect getExpandedButtonRect() {
      final RenderBox box = tester.renderObject<RenderBox>(find.byType(dropdownButtonType));
      final Rect buttonRect = box.localToGlobal(Offset.zero) & box.size;
      return buttonPadding.inflateRect(buttonRect);
    }

    Rect buttonRect;
    Rect menuRect;

    Future<Null> popUpAndDown(Widget frame) async {
      await tester.pumpWidget(frame);
      await tester.tap(find.byType(dropdownButtonType));
      await tester.pumpAndSettle();
      menuRect = getMenuRect();
      buttonRect = getExpandedButtonRect();
      await tester.tap(find.byType(dropdownButtonType));
    }

    // Dropdown button is along the top of the app. The top of the menu is
    // aligned with the top of the expanded button and shifted horizontally
    // so that it fits within the frame.

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.topLeft, value: menuItems.last)
    );
    expect(menuRect.topLeft, Offset.zero);
    expect(menuRect.topRight, new Offset(menuRect.width, 0.0));

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.topCenter, value: menuItems.last)
    );
    expect(menuRect.topLeft, new Offset(buttonRect.left, 0.0));
    expect(menuRect.topRight, new Offset(buttonRect.right, 0.0));

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.topRight, value: menuItems.last)
    );
    expect(menuRect.topLeft, new Offset(800.0 - menuRect.width, 0.0));
    expect(menuRect.topRight, const Offset(800.0, 0.0));

    // Dropdown button is along the middle of the app. The top of the menu is
    // aligned with the top of the expanded button (because the 1st item
    // is selected) and shifted horizontally so that it fits within the frame.

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.centerLeft, value: menuItems.first)
    );
    expect(menuRect.topLeft, new Offset(0.0, buttonRect.top));
    expect(menuRect.topRight, new Offset(menuRect.width, buttonRect.top));

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.center, value: menuItems.first)
    );
    expect(menuRect.topLeft, buttonRect.topLeft);
    expect(menuRect.topRight, buttonRect.topRight);

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.centerRight, value: menuItems.first)
    );
    expect(menuRect.topLeft, new Offset(800.0 - menuRect.width, buttonRect.top));
    expect(menuRect.topRight, new Offset(800.0, buttonRect.top));

    // Dropdown button is along the bottom of the app. The bottom of the menu is
    // aligned with the bottom of the expanded button and shifted horizontally
    // so that it fits within the frame.

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.bottomLeft, value: menuItems.first)
    );
    expect(menuRect.bottomLeft, const Offset(0.0, 600.0));
    expect(menuRect.bottomRight, new Offset(menuRect.width, 600.0));

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.bottomCenter, value: menuItems.first)
    );
    expect(menuRect.bottomLeft, new Offset(buttonRect.left, 600.0));
    expect(menuRect.bottomRight, new Offset(buttonRect.right, 600.0));

    await popUpAndDown(
      buildFrame(alignment: FractionalOffset.bottomRight, value: menuItems.first)
    );
    expect(menuRect.bottomLeft, new Offset(800.0 - menuRect.width, 600.0));
    expect(menuRect.bottomRight, const Offset(800.0, 600.0));
  });
469 470 471 472 473 474 475 476 477 478 479 480

  testWidgets('Dropdown menus are dismissed on screen orientation changes', (WidgetTester tester) async {
    await tester.pumpWidget(buildFrame());
    await tester.tap(find.byType(dropdownButtonType));
    await tester.pumpAndSettle();
    expect(find.byType(ListView), findsOneWidget);

    window.onMetricsChanged();
    await tester.pump();
    expect(find.byType(ListView, skipOffstage: false), findsNothing);
  });

481 482 483 484 485 486 487 488 489 490 491 492

  testWidgets('Semantics Tree contains only selected element', (WidgetTester tester) async {
    final SemanticsTester semantics = new SemanticsTester(tester);
    await tester.pumpWidget(buildFrame(items: menuItems));

    expect(semantics, isNot(includesNodeWith(label: menuItems[0])));
    expect(semantics, includesNodeWith(label: menuItems[1]));
    expect(semantics, isNot(includesNodeWith(label: menuItems[2])));
    expect(semantics, isNot(includesNodeWith(label: menuItems[3])));

    semantics.dispose();
  });
493
}