paginated_data_table_test.dart 38 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7 8 9
// TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=1000"
@Tags(<String>['no-shuffle'])
10
library;
11

12
import 'package:flutter/gestures.dart' show DragStartBehavior;
13
import 'package:flutter/material.dart';
14
import 'package:flutter/rendering.dart';
15 16
import 'package:flutter_test/flutter_test.dart';

17
import 'data_table_test_utils.dart';
18 19

class TestDataSource extends DataTableSource {
20
  TestDataSource({
21
    this.allowSelection = false,
22 23
  });

24
  final bool allowSelection;
25

26 27 28
  int get generation => _generation;
  int _generation = 0;
  set generation(int value) {
29
    if (_generation == value) {
30
      return;
31
    }
32 33 34 35
    _generation = value;
    notifyListeners();
  }

36 37 38
  final Set<int> _selectedRows = <int>{};

  void _handleSelected(int index, bool? selected) {
39
    if (selected ?? false) {
40 41 42 43 44 45 46
      _selectedRows.add(index);
    } else {
      _selectedRows.remove(index);
    }
    notifyListeners();
  }

47 48 49 50
  @override
  DataRow getRow(int index) {
    final Dessert dessert = kDesserts[index % kDesserts.length];
    final int page = index ~/ kDesserts.length;
51
    return DataRow.byIndex(
52
      index: index,
53
      selected: _selectedRows.contains(index),
54
      cells: <DataCell>[
55 56 57
        DataCell(Text('${dessert.name} ($page)')),
        DataCell(Text('${dessert.calories}')),
        DataCell(Text('$generation')),
58
      ],
59
      onSelectChanged: allowSelection ? (bool? selected) => _handleSelected(index, selected) : null,
60 61 62 63
    );
  }

  @override
64
  int get rowCount => 50 * kDesserts.length;
65 66 67 68 69

  @override
  bool get isRowCountApproximate => false;

  @override
70
  int get selectedRowCount => _selectedRows.length;
71 72 73
}

void main() {
74
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
75

76
  testWidgets('PaginatedDataTable paging', (WidgetTester tester) async {
77
    final TestDataSource source = TestDataSource();
78

79
    final List<String> log = <String>[];
80

81 82
    await tester.pumpWidget(MaterialApp(
      home: PaginatedDataTable(
83
        header: const Text('Test table'),
84 85
        source: source,
        rowsPerPage: 2,
86
        showFirstLastButtons: true,
87
        availableRowsPerPage: const <int>[
88 89
          2, 4, 8, 16,
        ],
90
        onRowsPerPageChanged: (int? rowsPerPage) {
91 92 93 94 95
          log.add('rows-per-page-changed: $rowsPerPage');
        },
        onPageChanged: (int rowIndex) {
          log.add('page-changed: $rowIndex');
        },
96
        columns: const <DataColumn>[
97 98 99
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
100
        ],
101
      ),
102 103 104 105 106 107 108 109 110 111 112 113 114
    ));

    await tester.tap(find.byTooltip('Next page'));

    expect(log, <String>['page-changed: 2']);
    log.clear();

    await tester.pump();

    expect(find.text('Frozen yogurt (0)'), findsNothing);
    expect(find.text('Eclair (0)'), findsOneWidget);
    expect(find.text('Gingerbread (0)'), findsNothing);

115
    await tester.tap(find.byIcon(Icons.chevron_left));
116 117 118 119 120 121 122 123 124 125

    expect(log, <String>['page-changed: 0']);
    log.clear();

    await tester.pump();

    expect(find.text('Frozen yogurt (0)'), findsOneWidget);
    expect(find.text('Eclair (0)'), findsNothing);
    expect(find.text('Gingerbread (0)'), findsNothing);

126 127 128 129
    final Finder lastPageButton = find.ancestor(
      of: find.byTooltip('Last page'),
      matching: find.byWidgetPredicate((Widget widget) => widget is IconButton),
    );
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145

    expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);

    await tester.tap(lastPageButton);

    expect(log, <String>['page-changed: 498']);
    log.clear();

    await tester.pump();

    expect(tester.widget<IconButton>(lastPageButton).onPressed, isNull);

    expect(find.text('Frozen yogurt (0)'), findsNothing);
    expect(find.text('Donut (49)'), findsOneWidget);
    expect(find.text('KitKat (49)'), findsOneWidget);

146 147 148 149
    final Finder firstPageButton = find.ancestor(
      of: find.byTooltip('First page'),
      matching: find.byWidgetPredicate((Widget widget) => widget is IconButton),
    );
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165

    expect(tester.widget<IconButton>(firstPageButton).onPressed, isNotNull);

    await tester.tap(firstPageButton);

    expect(log, <String>['page-changed: 0']);
    log.clear();

    await tester.pump();

    expect(tester.widget<IconButton>(firstPageButton).onPressed, isNull);

    expect(find.text('Frozen yogurt (0)'), findsOneWidget);
    expect(find.text('Eclair (0)'), findsNothing);
    expect(find.text('Gingerbread (0)'), findsNothing);

166
    await tester.tap(find.byIcon(Icons.chevron_left));
167 168 169 170

    expect(log, isEmpty);

    await tester.tap(find.text('2'));
171
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
172 173

    await tester.tap(find.text('8').last);
174
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
175 176 177 178 179

    expect(log, <String>['rows-per-page-changed: 8']);
    log.clear();
  });

180
  testWidgets('PaginatedDataTable control test', (WidgetTester tester) async {
181
    TestDataSource source = TestDataSource()
182 183
      ..generation = 42;

184
    final List<String> log = <String>[];
185 186

    Widget buildTable(TestDataSource source) {
187
      return PaginatedDataTable(
188
        header: const Text('Test table'),
189 190 191 192 193
        source: source,
        onPageChanged: (int rowIndex) {
          log.add('page-changed: $rowIndex');
        },
        columns: <DataColumn>[
194
          const DataColumn(
195
            label: Text('Name'),
196 197
            tooltip: 'Name',
          ),
198
          DataColumn(
199
            label: const Text('Calories'),
200 201 202 203
            tooltip: 'Calories',
            numeric: true,
            onSort: (int columnIndex, bool ascending) {
              log.add('column-sort: $columnIndex $ascending');
204
            },
205
          ),
206
          const DataColumn(
207
            label: Text('Generation'),
208 209 210 211
            tooltip: 'Generation',
          ),
        ],
        actions: <Widget>[
212
          IconButton(
213
            icon: const Icon(Icons.adjust),
214 215 216 217 218 219 220 221
            onPressed: () {
              log.add('action: adjust');
            },
          ),
        ],
      );
    }

222
    await tester.pumpWidget(MaterialApp(
223 224 225
      home: buildTable(source),
    ));

226
    // the column overflows because we're forcing it to 600 pixels high
227
    final dynamic exception = tester.takeException();
Dan Field's avatar
Dan Field committed
228
    expect(exception, isFlutterError);
229
    // ignore: avoid_dynamic_calls
230
    expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
231
    // ignore: avoid_dynamic_calls
232
    expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
233

234 235
    expect(find.text('Gingerbread (0)'), findsOneWidget);
    expect(find.text('Gingerbread (1)'), findsNothing);
236 237 238 239 240 241 242 243
    expect(find.text('42'), findsNWidgets(10));

    source.generation = 43;
    await tester.pump();

    expect(find.text('42'), findsNothing);
    expect(find.text('43'), findsNWidgets(10));

244
    source = TestDataSource()
245 246
      ..generation = 15;

247
    await tester.pumpWidget(MaterialApp(
248 249 250 251 252 253 254
      home: buildTable(source),
    ));

    expect(find.text('42'), findsNothing);
    expect(find.text('43'), findsNothing);
    expect(find.text('15'), findsNWidgets(10));

255
    final PaginatedDataTableState state = tester.state(find.byType(PaginatedDataTable));
256 257 258 259 260 261 262 263

    expect(log, isEmpty);
    state.pageTo(23);
    expect(log, <String>['page-changed: 20']);
    log.clear();

    await tester.pump();

264 265 266
    expect(find.text('Gingerbread (0)'), findsNothing);
    expect(find.text('Gingerbread (1)'), findsNothing);
    expect(find.text('Gingerbread (2)'), findsOneWidget);
267

268
    await tester.tap(find.byIcon(Icons.adjust));
269 270 271
    expect(log, <String>['action: adjust']);
    log.clear();
  });
272 273

  testWidgets('PaginatedDataTable text alignment', (WidgetTester tester) async {
274 275
    await tester.pumpWidget(MaterialApp(
      home: PaginatedDataTable(
276
        header: const Text('HEADER'),
277
        source: TestDataSource(),
278
        rowsPerPage: 8,
279
        availableRowsPerPage: const <int>[
280 281
          8, 9,
        ],
282
        onRowsPerPageChanged: (int? rowsPerPage) { },
283
        columns: const <DataColumn>[
284 285 286
          DataColumn(label: Text('COL1')),
          DataColumn(label: Text('COL2')),
          DataColumn(label: Text('COL3')),
287 288 289 290 291 292 293 294
        ],
      ),
    ));
    expect(find.text('Rows per page:'), findsOneWidget);
    expect(find.text('8'), findsOneWidget);
    expect(tester.getTopRight(find.text('8')).dx, tester.getTopRight(find.text('Rows per page:')).dx + 40.0); // per spec
  });

295 296 297 298 299 300 301 302 303 304
  testWidgets('PaginatedDataTable with and without header and actions', (WidgetTester tester) async {
    await binding.setSurfaceSize(const Size(800, 800));
    const String headerText = 'HEADER';
    final List<Widget> actions = <Widget>[
      IconButton(onPressed: () {}, icon: const Icon(Icons.add)),
    ];
    Widget buildTable({String? header, List<Widget>? actions}) => MaterialApp(
      home: PaginatedDataTable(
        header: header != null ? Text(header) : null,
        actions: actions,
305
        source: TestDataSource(allowSelection: true),
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
      ),
    );

    await tester.pumpWidget(buildTable(header: headerText));
    expect(find.text(headerText), findsOneWidget);
    expect(find.byIcon(Icons.add), findsNothing);

    await tester.pumpWidget(buildTable(header: headerText, actions: actions));
    expect(find.text(headerText), findsOneWidget);
    expect(find.byIcon(Icons.add), findsOneWidget);

    await tester.pumpWidget(buildTable());
    expect(find.text(headerText), findsNothing);
    expect(find.byIcon(Icons.add), findsNothing);

    expect(() => buildTable(actions: actions), throwsAssertionError);

    await binding.setSurfaceSize(null);
  });

331
  testWidgets('PaginatedDataTable with large text', (WidgetTester tester) async {
332 333 334
    final TestDataSource source = TestDataSource();
    await tester.pumpWidget(MaterialApp(
      home: MediaQuery(
335 336 337
        data: const MediaQueryData(
          textScaleFactor: 20.0,
        ),
338
        child: PaginatedDataTable(
339 340 341
          header: const Text('HEADER'),
          source: source,
          rowsPerPage: 501,
342
          availableRowsPerPage: const <int>[ 501 ],
343
          onRowsPerPageChanged: (int? rowsPerPage) { },
344
          columns: const <DataColumn>[
345 346 347
            DataColumn(label: Text('COL1')),
            DataColumn(label: Text('COL2')),
            DataColumn(label: Text('COL3')),
348 349 350 351 352
          ],
        ),
      ),
    ));
    // the column overflows because we're forcing it to 600 pixels high
353
    final dynamic exception = tester.takeException();
Dan Field's avatar
Dan Field committed
354
    expect(exception, isFlutterError);
355
    // ignore: avoid_dynamic_calls
356
    expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
357
    // ignore: avoid_dynamic_calls
358 359
    expect(exception.diagnostics.first.toString(), contains('A RenderFlex overflowed by'));

360 361 362 363 364 365
    expect(find.text('Rows per page:'), findsOneWidget);
    // Test that we will show some options in the drop down even if the lowest option is bigger than the source:
    assert(501 > source.rowCount);
    expect(find.text('501'), findsOneWidget);
    // Test that it fits:
    expect(tester.getTopRight(find.text('501')).dx, greaterThanOrEqualTo(tester.getTopRight(find.text('Rows per page:')).dx + 40.0));
366
  }, skip: isBrowser);  // https://github.com/flutter/flutter/issues/43433
367 368

  testWidgets('PaginatedDataTable footer scrolls', (WidgetTester tester) async {
369
    final TestDataSource source = TestDataSource();
370 371 372 373 374 375 376 377 378 379 380 381
    await tester.pumpWidget(
      MaterialApp(
        home: Align(
          alignment: Alignment.topLeft,
          child: SizedBox(
            width: 100.0,
            child: PaginatedDataTable(
              header: const Text('HEADER'),
              source: source,
              rowsPerPage: 5,
              dragStartBehavior: DragStartBehavior.down,
              availableRowsPerPage: const <int>[ 5 ],
382
              onRowsPerPageChanged: (int? rowsPerPage) { },
383 384 385 386 387 388
              columns: const <DataColumn>[
                DataColumn(label: Text('COL1')),
                DataColumn(label: Text('COL2')),
                DataColumn(label: Text('COL3')),
              ],
            ),
389 390 391
          ),
        ),
      ),
392
    );
393 394 395
    expect(find.text('Rows per page:'), findsOneWidget);
    expect(tester.getTopLeft(find.text('Rows per page:')).dx, lessThan(0.0)); // off screen
    await tester.dragFrom(
396
      Offset(50.0, tester.getTopLeft(find.text('Rows per page:')).dy),
397 398 399 400 401 402
      const Offset(1000.0, 0.0),
    );
    await tester.pump();
    expect(find.text('Rows per page:'), findsOneWidget);
    expect(tester.getTopLeft(find.text('Rows per page:')).dx, 18.0); // 14 padding in the footer row, 4 padding from the card
  });
403

404 405 406 407
  testWidgets('PaginatedDataTable custom row height', (WidgetTester tester) async {
    final TestDataSource source = TestDataSource();

    Widget buildCustomHeightPaginatedTable({
408 409 410
      double? dataRowHeight,
      double? dataRowMinHeight,
      double? dataRowMaxHeight,
411 412 413 414 415 416 417 418 419
      double headingRowHeight = 56.0,
    }) {
      return PaginatedDataTable(
        header: const Text('Test table'),
        source: source,
        rowsPerPage: 2,
        availableRowsPerPage: const <int>[
          2, 4, 8, 16,
        ],
420
        onRowsPerPageChanged: (int? rowsPerPage) {},
421 422 423 424 425 426 427
        onPageChanged: (int rowIndex) {},
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
        dataRowHeight: dataRowHeight,
428 429
        dataRowMinHeight: dataRowMinHeight,
        dataRowMaxHeight: dataRowMaxHeight,
430 431 432 433 434 435 436 437 438 439 440 441 442
        headingRowHeight: headingRowHeight,
      );
    }

    // DEFAULT VALUES
    await tester.pumpWidget(MaterialApp(
      home: PaginatedDataTable(
        header: const Text('Test table'),
        source: source,
        rowsPerPage: 2,
        availableRowsPerPage: const <int>[
          2, 4, 8, 16,
        ],
443
        onRowsPerPageChanged: (int? rowsPerPage) {},
444 445 446 447 448 449 450 451 452
        onPageChanged: (int rowIndex) {},
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
      ),
    ));
    expect(tester.renderObject<RenderBox>(
453
      find.widgetWithText(Container, 'Name').first,
454 455
    ).size.height, 56.0); // This is the header row height
    expect(tester.renderObject<RenderBox>(
456
      find.widgetWithText(Container, 'Frozen yogurt (0)').first,
457 458 459 460 461 462 463
    ).size.height, 48.0); // This is the data row height

    // CUSTOM VALUES
    await tester.pumpWidget(MaterialApp(
      home: Material(child: buildCustomHeightPaginatedTable(headingRowHeight: 48.0)),
    ));
    expect(tester.renderObject<RenderBox>(
464
      find.widgetWithText(Container, 'Name').first,
465 466 467 468 469 470
    ).size.height, 48.0);

    await tester.pumpWidget(MaterialApp(
      home: Material(child: buildCustomHeightPaginatedTable(headingRowHeight: 64.0)),
    ));
    expect(tester.renderObject<RenderBox>(
471
      find.widgetWithText(Container, 'Name').first,
472 473 474 475 476 477
    ).size.height, 64.0);

    await tester.pumpWidget(MaterialApp(
      home: Material(child: buildCustomHeightPaginatedTable(dataRowHeight: 30.0)),
    ));
    expect(tester.renderObject<RenderBox>(
478
      find.widgetWithText(Container, 'Frozen yogurt (0)').first,
479 480 481 482 483 484
    ).size.height, 30.0);

    await tester.pumpWidget(MaterialApp(
      home: Material(child: buildCustomHeightPaginatedTable(dataRowHeight: 56.0)),
    ));
    expect(tester.renderObject<RenderBox>(
485
      find.widgetWithText(Container, 'Frozen yogurt (0)').first,
486
    ).size.height, 56.0);
487 488 489 490 491 492 493

    await tester.pumpWidget(MaterialApp(
      home: Material(child: buildCustomHeightPaginatedTable(dataRowMinHeight: 51.0, dataRowMaxHeight: 51.0)),
    ));
    expect(tester.renderObject<RenderBox>(
      find.widgetWithText(Container, 'Frozen yogurt (0)').first,
    ).size.height, 51.0);
494
  });
495 496

  testWidgets('PaginatedDataTable custom horizontal padding - checkbox', (WidgetTester tester) async {
497 498 499 500
    const double defaultHorizontalMargin = 24.0;
    const double defaultColumnSpacing = 56.0;
    const double customHorizontalMargin = 10.0;
    const double customColumnSpacing = 15.0;
501

502 503
    const double width = 400;
    const double height = 400;
504 505 506 507 508

    final Size originalSize = binding.renderView.size;

    // Ensure the containing Card is small enough that we don't expand too
    // much, resulting in our custom margin being ignored.
509
    await binding.setSurfaceSize(const Size(width, height));
510

511
    final TestDataSource source = TestDataSource(allowSelection: true);
512 513 514 515 516 517 518 519 520 521 522 523
    Finder cellContent;
    Finder checkbox;
    Finder padding;

    await tester.pumpWidget(MaterialApp(
      home: PaginatedDataTable(
        header: const Text('Test table'),
        source: source,
        rowsPerPage: 2,
        availableRowsPerPage: const <int>[
          2, 4,
        ],
524
        onRowsPerPageChanged: (int? rowsPerPage) {},
525
        onPageChanged: (int rowIndex) {},
526
        onSelectAll: (bool? value) {},
527 528 529 530 531 532 533 534 535 536 537 538 539
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
      ),
    ));

    // default checkbox padding
    checkbox = find.byType(Checkbox).first;
    padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first;
    expect(
      tester.getRect(checkbox).left - tester.getRect(padding).left,
540
      defaultHorizontalMargin,
541 542 543
    );
    expect(
      tester.getRect(padding).right - tester.getRect(checkbox).right,
544
      defaultHorizontalMargin / 2,
545 546 547 548 549 550 551
    );

    // default first column padding
    padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
    cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)'); // DataTable wraps its DataCells in an Align widget
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
552
      defaultHorizontalMargin / 2,
553 554 555
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
556
      defaultColumnSpacing / 2,
557 558 559 560 561 562 563
    );

    // default middle column padding
    padding = find.widgetWithText(Padding, '159').first;
    cellContent = find.widgetWithText(Align, '159');
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
564
      defaultColumnSpacing / 2,
565 566 567
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
568
      defaultColumnSpacing / 2,
569 570 571 572 573 574 575
    );

    // default last column padding
    padding = find.widgetWithText(Padding, '0').first;
    cellContent = find.widgetWithText(Align, '0').first;
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
576
      defaultColumnSpacing / 2,
577 578 579
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
580
      defaultHorizontalMargin,
581 582 583 584 585 586 587 588 589 590 591 592
    );

    // CUSTOM VALUES
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: PaginatedDataTable(
          header: const Text('Test table'),
          source: source,
          rowsPerPage: 2,
          availableRowsPerPage: const <int>[
            2, 4,
          ],
593
          onRowsPerPageChanged: (int? rowsPerPage) {},
594
          onPageChanged: (int rowIndex) {},
595
          onSelectAll: (bool? value) {},
596 597 598 599 600
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
601 602
          horizontalMargin: customHorizontalMargin,
          columnSpacing: customColumnSpacing,
603 604 605 606 607 608 609 610 611
        ),
      ),
    ));

    // custom checkbox padding
    checkbox = find.byType(Checkbox).first;
    padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first;
    expect(
      tester.getRect(checkbox).left - tester.getRect(padding).left,
612
      customHorizontalMargin,
613 614 615
    );
    expect(
      tester.getRect(padding).right - tester.getRect(checkbox).right,
616
      customHorizontalMargin / 2,
617 618 619 620 621 622 623
    );

    // custom first column padding
    padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
    cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)'); // DataTable wraps its DataCells in an Align widget
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
624
      customHorizontalMargin / 2,
625 626 627
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
628
      customColumnSpacing / 2,
629 630 631 632 633 634 635
    );

    // custom middle column padding
    padding = find.widgetWithText(Padding, '159').first;
    cellContent = find.widgetWithText(Align, '159');
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
636
      customColumnSpacing / 2,
637 638 639
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
640
      customColumnSpacing / 2,
641 642 643 644 645 646 647
    );

    // custom last column padding
    padding = find.widgetWithText(Padding, '0').first;
    cellContent = find.widgetWithText(Align, '0').first;
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
648
      customColumnSpacing / 2,
649 650 651
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
652
      customHorizontalMargin,
653
    );
654 655 656

    // Reset the surface size.
    await binding.setSurfaceSize(originalSize);
657 658 659
  });

  testWidgets('PaginatedDataTable custom horizontal padding - no checkbox', (WidgetTester tester) async {
660 661 662 663
    const double defaultHorizontalMargin = 24.0;
    const double defaultColumnSpacing = 56.0;
    const double customHorizontalMargin = 10.0;
    const double customColumnSpacing = 15.0;
664 665 666 667 668 669 670 671 672 673 674 675
    final TestDataSource source = TestDataSource();
    Finder cellContent;
    Finder padding;

    await tester.pumpWidget(MaterialApp(
      home: PaginatedDataTable(
        header: const Text('Test table'),
        source: source,
        rowsPerPage: 2,
        availableRowsPerPage: const <int>[
          2, 4, 8, 16,
        ],
676
        onRowsPerPageChanged: (int? rowsPerPage) {},
677 678 679 680 681 682 683 684 685 686 687 688 689 690
        onPageChanged: (int rowIndex) {},
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
      ),
    ));

    // default first column padding
    padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
    cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)'); // DataTable wraps its DataCells in an Align widget
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
691
      defaultHorizontalMargin,
692 693 694
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
695
      defaultColumnSpacing / 2,
696 697 698 699 700 701 702
    );

    // default middle column padding
    padding = find.widgetWithText(Padding, '159').first;
    cellContent = find.widgetWithText(Align, '159');
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
703
      defaultColumnSpacing / 2,
704 705 706
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
707
      defaultColumnSpacing / 2,
708 709 710 711 712 713 714
    );

    // default last column padding
    padding = find.widgetWithText(Padding, '0').first;
    cellContent = find.widgetWithText(Align, '0').first;
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
715
      defaultColumnSpacing / 2,
716 717 718
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
719
      defaultHorizontalMargin,
720 721 722 723 724 725 726 727 728 729 730 731
    );

    // CUSTOM VALUES
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: PaginatedDataTable(
          header: const Text('Test table'),
          source: source,
          rowsPerPage: 2,
          availableRowsPerPage: const <int>[
            2, 4, 8, 16,
          ],
732
          onRowsPerPageChanged: (int? rowsPerPage) {},
733 734 735 736 737 738
          onPageChanged: (int rowIndex) {},
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
739 740
          horizontalMargin: customHorizontalMargin,
          columnSpacing: customColumnSpacing,
741 742 743 744 745 746 747 748 749
        ),
      ),
    ));

    // custom first column padding
    padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
    cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)');
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
750
      customHorizontalMargin,
751 752 753
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
754
      customColumnSpacing / 2,
755 756 757 758 759 760 761
    );

    // custom middle column padding
    padding = find.widgetWithText(Padding, '159').first;
    cellContent = find.widgetWithText(Align, '159');
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
762
      customColumnSpacing / 2,
763 764 765
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
766
      customColumnSpacing / 2,
767 768 769 770 771 772 773
    );

    // custom last column padding
    padding = find.widgetWithText(Padding, '0').first;
    cellContent = find.widgetWithText(Align, '0').first;
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
774
      customColumnSpacing / 2,
775 776 777
    );
    expect(
      tester.getRect(padding).right - tester.getRect(cellContent).right,
778
      customHorizontalMargin,
779 780
    );
  });
781 782 783 784

  testWidgets('PaginatedDataTable table fills Card width', (WidgetTester tester) async {
    final TestDataSource source = TestDataSource();

785
    // 800 is wide enough to ensure that all of the columns fit in the
786 787
    // Card. The test makes sure that the DataTable is exactly as wide
    // as the Card, minus the Card's margin.
788 789 790
    const double originalWidth = 800;
    const double expandedWidth = 1600;
    const double height = 400;
791

792
    // By default, the margin of a Card is 4 in all directions, so
apeltop's avatar
apeltop committed
793
    // the size of the DataTable (inside the Card) is horizontally
794
    // reduced by 4 * 2; the left and right margins.
795
    const double cardMargin = 8;
796

797 798 799 800 801 802 803 804 805 806
    final Size originalSize = binding.renderView.size;

    Widget buildWidget() => MaterialApp(
      home: PaginatedDataTable(
        header: const Text('Test table'),
        source: source,
        rowsPerPage: 2,
        availableRowsPerPage: const <int>[
          2, 4, 8, 16,
        ],
807
        onRowsPerPageChanged: (int? rowsPerPage) {},
808 809 810 811 812 813 814 815 816
        onPageChanged: (int rowIndex) {},
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
      ),
    );

817
    await binding.setSurfaceSize(const Size(originalWidth, height));
818 819
    await tester.pumpWidget(buildWidget());

820 821
    double cardWidth = tester.renderObject<RenderBox>(find.byType(Card).first).size.width;

822 823 824
    // Widths should be equal before we resize...
    expect(
      tester.renderObject<RenderBox>(find.byType(DataTable).first).size.width,
825
      moreOrLessEquals(cardWidth - cardMargin),
826 827
    );

828
    await binding.setSurfaceSize(const Size(expandedWidth, height));
829 830
    await tester.pumpWidget(buildWidget());

831
    cardWidth = tester.renderObject<RenderBox>(find.byType(Card).first).size.width;
832 833 834 835

    // ... and should still be equal after the resize.
    expect(
      tester.renderObject<RenderBox>(find.byType(DataTable).first).size.width,
836
      moreOrLessEquals(cardWidth - cardMargin),
837 838 839
    );

    // Double check to ensure we actually resized the surface properly.
840
    expect(cardWidth, moreOrLessEquals(expandedWidth));
841 842 843 844

    // Reset the surface size.
    await binding.setSurfaceSize(originalSize);
  });
845 846 847 848 849 850 851

  testWidgets('PaginatedDataTable with optional column checkbox', (WidgetTester tester) async {
    await binding.setSurfaceSize(const Size(800, 800));

    Widget buildTable(bool checkbox) => MaterialApp(
      home: PaginatedDataTable(
        header: const Text('Test table'),
852
        source: TestDataSource(allowSelection: true),
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
        showCheckboxColumn: checkbox,
        columns: const <DataColumn>[
          DataColumn(label: Text('Name')),
          DataColumn(label: Text('Calories'), numeric: true),
          DataColumn(label: Text('Generation')),
        ],
      ),
    );

    await tester.pumpWidget(buildTable(true));
    expect(find.byType(Checkbox), findsNWidgets(11));

    await tester.pumpWidget(buildTable(false));
    expect(find.byType(Checkbox), findsNothing);
  });
868 869 870 871 872 873 874 875 876 877 878 879 880 881

  testWidgets('Table should not use decoration from DataTableTheme', (WidgetTester tester) async {
    final Size originalSize = binding.renderView.size;
    await binding.setSurfaceSize(const Size(800, 800));

    Widget buildTable() {
      return MaterialApp(
        theme: ThemeData.light().copyWith(
            dataTableTheme: const DataTableThemeData(
              decoration: BoxDecoration(color: Colors.white),
            ),
        ),
        home: PaginatedDataTable(
          header: const Text('Test table'),
882
          source: TestDataSource(allowSelection: true),
883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
        ),
      );
    }

    await tester.pumpWidget(buildTable());
    final Finder tableContainerFinder = find.ancestor(of: find.byType(Table), matching: find.byType(Container)).first;
    expect(tester.widget<Container>(tableContainerFinder).decoration, const BoxDecoration());

    // Reset the surface size.
    await binding.setSurfaceSize(originalSize);
  });
899 900

  testWidgets('PaginatedDataTable custom checkboxHorizontalMargin properly applied', (WidgetTester tester) async {
901 902
    const double customCheckboxHorizontalMargin = 15.0;
    const double customHorizontalMargin = 10.0;
903

904 905
    const double width = 400;
    const double height = 400;
906 907 908 909 910

    final Size originalSize = binding.renderView.size;

    // Ensure the containing Card is small enough that we don't expand too
    // much, resulting in our custom margin being ignored.
911
    await binding.setSurfaceSize(const Size(width, height));
912

913
    final TestDataSource source = TestDataSource(allowSelection: true);
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
    Finder cellContent;
    Finder checkbox;
    Finder padding;

    // CUSTOM VALUES
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: PaginatedDataTable(
          header: const Text('Test table'),
          source: source,
          rowsPerPage: 2,
          availableRowsPerPage: const <int>[
            2, 4,
          ],
          onRowsPerPageChanged: (int? rowsPerPage) {},
          onPageChanged: (int rowIndex) {},
          onSelectAll: (bool? value) {},
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
936 937
          horizontalMargin: customHorizontalMargin,
          checkboxHorizontalMargin: customCheckboxHorizontalMargin,
938 939 940 941 942 943 944 945 946
        ),
      ),
    ));

    // Custom checkbox padding.
    checkbox = find.byType(Checkbox).first;
    padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first;
    expect(
      tester.getRect(checkbox).left - tester.getRect(padding).left,
947
      customCheckboxHorizontalMargin,
948 949 950
    );
    expect(
      tester.getRect(padding).right - tester.getRect(checkbox).right,
951
      customCheckboxHorizontalMargin,
952 953 954 955 956 957 958
    );

    // Custom first column padding.
    padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
    cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)'); // DataTable wraps its DataCells in an Align widget.
    expect(
      tester.getRect(cellContent).left - tester.getRect(padding).left,
959
      customHorizontalMargin,
960 961 962 963 964
    );

    // Reset the surface size.
    await binding.setSurfaceSize(originalSize);
  });
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003

  testWidgets('Items selected text uses secondary color', (WidgetTester tester) async {
    const Color selectedTextColor = Color(0xff00ddff);
    final ColorScheme colors = const ColorScheme.light().copyWith(secondary: selectedTextColor);
    final ThemeData theme = ThemeData.from(colorScheme: colors);

    Widget buildTable() {
      return MaterialApp(
        theme: theme,
        home: PaginatedDataTable(
          header: const Text('Test table'),
          source: TestDataSource(allowSelection: true),
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
        ),
      );
    }

    await binding.setSurfaceSize(const Size(800, 800));
    await tester.pumpWidget(buildTable());
    expect(find.text('Test table'), findsOneWidget);

    // Select a row with yogurt
    await tester.tap(find.text('Frozen yogurt (0)'));
    await tester.pumpAndSettle();

    // The header should be replace with a selected text item
    expect(find.text('Test table'), findsNothing);
    expect(find.text('1 item selected'), findsOneWidget);

    // The color of the selected text item should be the colorScheme.secondary
    final TextStyle selectedTextStyle = tester.renderObject<RenderParagraph>(find.text('1 item selected')).text.style!;
    expect(selectedTextStyle.color, equals(selectedTextColor));

    await binding.setSurfaceSize(null);
  });
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031

  testWidgets('PaginatedDataTable arrowHeadColor set properly', (WidgetTester tester) async {
    await binding.setSurfaceSize(const Size(800, 800));
    const Color arrowHeadColor = Color(0xFFE53935);

    await tester.pumpWidget(
      MaterialApp(
        home: PaginatedDataTable(
          arrowHeadColor: arrowHeadColor,
          showFirstLastButtons: true,
          header: const Text('Test table'),
          source: TestDataSource(),
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
        ),
      )
    );

    final Iterable<Icon> icons = tester.widgetList(find.byType(Icon));

    expect(icons.elementAt(0).color, arrowHeadColor);
    expect(icons.elementAt(1).color, arrowHeadColor);
    expect(icons.elementAt(2).color, arrowHeadColor);
    expect(icons.elementAt(3).color, arrowHeadColor);
  });
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059

  testWidgets('OverflowBar header left alignment', (WidgetTester tester) async {
    // Test an old special case that tried to align the first child of a ButtonBar
    // and the left edge of a Text header widget. Still possible with OverflowBar
    // albeit without any special case in the implementation's build method.
    Widget buildFrame(Widget header) {
      return MaterialApp(
        home: PaginatedDataTable(
          header: header,
          rowsPerPage: 2,
          source: TestDataSource(),
          columns: const <DataColumn>[
            DataColumn(label: Text('Name')),
            DataColumn(label: Text('Calories'), numeric: true),
            DataColumn(label: Text('Generation')),
          ],
        ),
      );
    }

    await tester.pumpWidget(buildFrame(const Text('HEADER')));
    final double headerX = tester.getTopLeft(find.text('HEADER')).dx;
    final Widget overflowBar = OverflowBar(
      children: <Widget>[ElevatedButton(onPressed: () {}, child: const Text('BUTTON'))],
    );
    await tester.pumpWidget(buildFrame(overflowBar));
    expect(headerX, tester.getTopLeft(find.byType(ElevatedButton)).dx);
  });
1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140

  testWidgets('PaginatedDataTable can be scrolled using ScrollController', (WidgetTester tester) async {
    final TestDataSource source = TestDataSource();
    final ScrollController scrollController = ScrollController();

    Widget buildTable(TestDataSource source) {
      return Align(
        alignment: Alignment.topLeft,
        child: SizedBox(
          width: 100,
          child: PaginatedDataTable(
            controller: scrollController,
            header: const Text('Test table'),
            source: source,
            rowsPerPage: 2,
            columns: const <DataColumn>[
              DataColumn(
                label: Text('Name'),
                tooltip: 'Name',
              ),
              DataColumn(
                label: Text('Calories'),
                tooltip: 'Calories',
                numeric: true,
              ),
              DataColumn(
                label: Text('Generation'),
                tooltip: 'Generation',
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(MaterialApp(
      home: buildTable(source),
    ));

    // DataTable uses provided ScrollController
    final Scrollable bodyScrollView = tester.widget(find.byType(Scrollable).first);
    expect(bodyScrollView.controller, scrollController);

    expect(scrollController.offset, 0.0);
    scrollController.jumpTo(50.0);
    await tester.pumpAndSettle();

    expect(scrollController.offset, 50.0);
  });

  testWidgets('PaginatedDataTable uses PrimaryScrollController when primary ', (WidgetTester tester) async {
    final ScrollController primaryScrollController = ScrollController();
    final TestDataSource source = TestDataSource();

    await tester.pumpWidget(
      MaterialApp(
        home: PrimaryScrollController(
          controller: primaryScrollController,
          child: PaginatedDataTable(
            primary: true,
            header: const Text('Test table'),
            source: source,
            rowsPerPage: 2,
            columns: const <DataColumn>[
              DataColumn(label: Text('Name')),
              DataColumn(label: Text('Calories'), numeric: true),
              DataColumn(label: Text('Generation')),
            ],
          ),
        ),
      )
    );

    // DataTable uses primaryScrollController
    final Scrollable bodyScrollView = tester.widget(find.byType(Scrollable).first);
    expect(bodyScrollView.controller, primaryScrollController);

    // Footer does not use primaryScrollController
    final Scrollable footerScrollView = tester.widget(find.byType(Scrollable).last);
    expect(footerScrollView.controller, null);
  });
1141
}