physical_model_test.dart 17.5 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
import 'dart:math' as math show pi;
6

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

void main() {
12 13
  testWidgets('PhysicalModel updates clipBehavior in updateRenderObject', (WidgetTester tester) async {
    await tester.pumpWidget(
14
      const MaterialApp(home: PhysicalModel(color: Colors.red)),
15 16 17 18 19 20 21
    );

    final RenderPhysicalModel renderPhysicalModel = tester.allRenderObjects.whereType<RenderPhysicalModel>().first;

    expect(renderPhysicalModel.clipBehavior, equals(Clip.none));

    await tester.pumpWidget(
22
      const MaterialApp(home: PhysicalModel(clipBehavior: Clip.antiAlias, color: Colors.red)),
23 24 25 26 27 28 29
    );

    expect(renderPhysicalModel.clipBehavior, equals(Clip.antiAlias));
  });

  testWidgets('PhysicalShape updates clipBehavior in updateRenderObject', (WidgetTester tester) async {
    await tester.pumpWidget(
30
      const MaterialApp(home: PhysicalShape(color: Colors.red, clipper: ShapeBorderClipper(shape: CircleBorder()))),
31 32 33 34 35 36 37
    );

    final RenderPhysicalShape renderPhysicalShape = tester.allRenderObjects.whereType<RenderPhysicalShape>().first;

    expect(renderPhysicalShape.clipBehavior, equals(Clip.none));

    await tester.pumpWidget(
38
      const MaterialApp(home: PhysicalShape(clipBehavior: Clip.antiAlias, color: Colors.red, clipper: ShapeBorderClipper(shape: CircleBorder()))),
39 40 41 42 43
    );

    expect(renderPhysicalShape.clipBehavior, equals(Clip.antiAlias));
  });

44
  testWidgets('PhysicalModel - creates a physical model layer when it needs compositing', (WidgetTester tester) async {
45
    debugDisableShadows = false;
46
    await tester.pumpWidget(
47 48 49 50 51 52 53
      MaterialApp(
        home: PhysicalModel(
          shape: BoxShape.rectangle,
          color: Colors.grey,
          shadowColor: Colors.red,
          elevation: 1.0,
          child: Material(child: TextField(controller: TextEditingController())),
54
        ),
55
      ),
56
    );
57 58
    await tester.pump();

59
    final RenderPhysicalModel renderPhysicalModel = tester.allRenderObjects.whereType<RenderPhysicalModel>().first;
60 61
    expect(renderPhysicalModel.needsCompositing, true);

62
    final PhysicalModelLayer physicalModelLayer = tester.layers.whereType<PhysicalModelLayer>().first;
63 64 65
    expect(physicalModelLayer.shadowColor, Colors.red);
    expect(physicalModelLayer.color, Colors.grey);
    expect(physicalModelLayer.elevation, 1.0);
66
    debugDisableShadows = true;
67
  });
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91

  testWidgets('PhysicalModel - clips when overflows and elevation is 0', (WidgetTester tester) async {
    const Key key = Key('test');
    await tester.pumpWidget(
      MediaQuery(
        key: key,
        data: const MediaQueryData(devicePixelRatio: 1.0),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Row(
              children: const <Widget>[
                Material(child: Text('A long long long long long long long string')),
                Material(child: Text('A long long long long long long long string')),
                Material(child: Text('A long long long long long long long string')),
                Material(child: Text('A long long long long long long long string')),
              ],
            ),
          ),
        ),
      ),
    );

92
    final dynamic exception = tester.takeException();
Dan Field's avatar
Dan Field committed
93
    expect(exception, isFlutterError);
94
    // ignore: avoid_dynamic_calls
95
    expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
96
    // ignore: avoid_dynamic_calls
97
    expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
98 99
    await expectLater(
      find.byKey(key),
100
      matchesGoldenFile('physical_model_overflow.png'),
101
    );
102
  });
103 104 105 106 107

  group('PhysicalModelLayer checks elevation', () {
    Future<void> _testStackChildren(
      WidgetTester tester,
      List<Widget> children, {
108
      required int expectedErrorCount,
109 110 111 112 113 114 115 116 117 118
      bool enableCheck = true,
    }) async {
      assert(expectedErrorCount != null);
      if (enableCheck) {
        debugCheckElevationsEnabled = true;
      } else {
        assert(expectedErrorCount == 0, 'Cannot expect errors if check is disabled.');
      }
      debugDisableShadows = false;
      int count = 0;
119
      final void Function(FlutterErrorDetails)? oldOnError = FlutterError.onError;
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
      FlutterError.onError = (FlutterErrorDetails details) {
        count++;
      };
      await tester.pumpWidget(Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
            children: children,
          ),
        ),
      );
      FlutterError.onError = oldOnError;
      expect(count, expectedErrorCount);
      if (enableCheck) {
        debugCheckElevationsEnabled = false;
      }
      debugDisableShadows = true;
    }

    // Tests:
    //
    //        ───────────             (red rect, paints second, child)
    //              │
    //        ───────────             (green rect, paints first)
    //            │
    // ────────────────────────────
    testWidgets('entirely overlapping, direct child', (WidgetTester tester) async {
146 147
      const List<Widget> children = <Widget>[
        SizedBox(
148 149
          width: 300,
          height: 300,
150
          child: Material(
151 152 153 154 155
            elevation: 1.0,
            color: Colors.green,
            child: Material(
              elevation: 2.0,
              color: Colors.red,
156
            ),
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 0);
      expect(find.byType(Material), findsNWidgets(2));
    });


    // Tests:
    //
    //        ───────────────          (green rect, paints second)
    //        ─────────── │            (blue rect, paints first)
    //         │          │
    //         │          │
    // ────────────────────────────
    testWidgets('entirely overlapping, correct painting order', (WidgetTester tester) async {
174 175
      const List<Widget> children = <Widget>[
        SizedBox(
176 177
          width: 300,
          height: 300,
178
          child: Material(
179 180 181 182
            elevation: 1.0,
            color: Colors.green,
          ),
        ),
183
        SizedBox(
184 185
          width: 300,
          height: 300,
186
          child: Material(
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
            elevation: 2.0,
            color: Colors.blue,
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 0);
      expect(find.byType(Material), findsNWidgets(2));
    });


    // Tests:
    //
    //        ───────────────          (green rect, paints first)
    //         │  ───────────          (blue rect, paints second)
    //         │        │
    //         │        │
    // ────────────────────────────
    testWidgets('entirely overlapping, wrong painting order', (WidgetTester tester) async {
206 207
      const List<Widget> children = <Widget>[
        SizedBox(
208 209
          width: 300,
          height: 300,
210
          child: Material(
211 212 213 214
            elevation: 2.0,
            color: Colors.green,
          ),
        ),
215
        SizedBox(
216 217
          width: 300,
          height: 300,
218
          child: Material(
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
            elevation: 1.0,
            color: Colors.blue,
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 1);
      expect(find.byType(Material), findsNWidgets(2));
    });


    // Tests:
    //
    //  ───────────────                      (brown rect, paints first)
    //         │        ───────────          (red circle, paints second)
    //         │            │
    //         │            │
    // ────────────────────────────
    testWidgets('not non-rect not overlapping, wrong painting order', (WidgetTester tester) async {
      // These would be overlapping if we only took the rectangular bounds of the circle.
      final List<Widget> children = <Widget>[
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
241
          rect: const Rect.fromLTWH(150, 150, 150, 150),
242
          child: const SizedBox(
243 244
            width: 300,
            height: 300,
245
            child: Material(
246 247 248 249 250 251
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
252
          rect: const Rect.fromLTWH(20, 20, 140, 150),
253
          child: const SizedBox(
254 255
            width: 300,
            height: 300,
256
            child: Material(
257 258
              elevation: 2.0,
              color: Colors.red,
259
              shape: CircleBorder(),
260 261 262 263 264 265 266
            ),
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 0);
      expect(find.byType(Material), findsNWidgets(2));
267
    }, skip: isBrowser);  // https://github.com/flutter/flutter/issues/52855
268 269 270 271 272 273 274 275 276 277 278

    // Tests:
    //
    //        ───────────────          (brown rect, paints first)
    //         │  ───────────          (red circle, paints second)
    //         │        │
    //         │        │
    // ────────────────────────────
    testWidgets('not non-rect entirely overlapping, wrong painting order', (WidgetTester tester) async {
      final List<Widget> children = <Widget>[
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
279
          rect: const Rect.fromLTWH(20, 20, 140, 150),
280
          child: const SizedBox(
281 282
            width: 300,
            height: 300,
283
            child: Material(
284 285 286 287 288 289
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
290
          rect: const Rect.fromLTWH(50, 50, 100, 100),
291
          child: const SizedBox(
292 293
            width: 300,
            height: 300,
294
            child: Material(
295 296
              elevation: 2.0,
              color: Colors.red,
297
              shape: CircleBorder(),
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
            ),
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 1);
      expect(find.byType(Material), findsNWidgets(2));
    });

    // Tests:
    //
    //   ───────────────                 (brown rect, paints first)
    //         │      ────────────       (red circle, paints second)
    //         │           │
    //         │           │
    // ────────────────────────────
    testWidgets('non-rect partially overlapping, wrong painting order', (WidgetTester tester) async {
      final List<Widget> children = <Widget>[
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
317
          rect: const Rect.fromLTWH(150, 150, 150, 150),
318
          child: const SizedBox(
319 320
            width: 300,
            height: 300,
321
            child: Material(
322 323 324 325 326 327
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
328
          rect: const Rect.fromLTWH(30, 20, 150, 150),
329
          child: const SizedBox(
330 331
            width: 300,
            height: 300,
332
            child: Material(
333 334
              elevation: 2.0,
              color: Colors.red,
335
              shape: CircleBorder(),
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
            ),
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 1);
      expect(find.byType(Material), findsNWidgets(2));
    });

    // Tests:
    //
    //   ───────────────                 (green rect, paints second, overlaps red rect)
    //         │
    //         │
    //   ──────────────────────────      (brown and red rects, overlapping but same elevation, paint first and third)
    //         │           │
    // ────────────────────────────
    //
    // Fails because the green rect overlaps the
    testWidgets('child partially overlapping, wrong painting order', (WidgetTester tester) async {
      final List<Widget> children = <Widget>[
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
358
          rect: const Rect.fromLTWH(150, 150, 150, 150),
359
          child: const SizedBox(
360 361
            width: 300,
            height: 300,
362
            child: Material(
363 364 365 366 367 368 369 370 371 372 373 374 375
              elevation: 1.0,
              color: Colors.brown,
              child: Padding(
                padding: EdgeInsets.all(30.0),
                child: Material(
                  elevation: 2.0,
                  color: Colors.green,
                ),
              ),
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
376
          rect: const Rect.fromLTWH(30, 20, 180, 180),
377
          child: const SizedBox(
378 379
            width: 300,
            height: 300,
380
            child: Material(
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
              elevation: 1.0,
              color: Colors.red,
            ),
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 1);
      expect(find.byType(Material), findsNWidgets(3));
    });

    // Tests:
    //
    //   ───────────────                 (brown rect, paints first)
    //         │      ────────────       (red circle, paints second)
    //         │           │
    //         │           │
    // ────────────────────────────
    testWidgets('non-rect partially overlapping, wrong painting order, check disabled', (WidgetTester tester) async {
400
      final List<Widget> children = <Widget>[
401
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
402
          rect: const Rect.fromLTWH(150, 150, 150, 150),
403
          child: const SizedBox(
404 405
            width: 300,
            height: 300,
406
            child: Material(
407 408 409 410 411 412
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
413
          rect: const Rect.fromLTWH(30, 20, 150, 150),
414
          child: const SizedBox(
415 416
            width: 300,
            height: 300,
417
            child: Material(
418 419
              elevation: 2.0,
              color: Colors.red,
420
              shape: CircleBorder(),
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
            ),
          ),
        ),
      ];

      await _testStackChildren(
        tester,
        children,
        expectedErrorCount: 0,
        enableCheck: false,
      );
      expect(find.byType(Material), findsNWidgets(2));
    });

    // Tests:
    //
    //   ────────────                    (brown rect, paints first, rotated but doesn't overlap)
    //         │      ────────────       (red circle, paints second)
    //         │           │
    //         │           │
    // ────────────────────────────
    testWidgets('with a RenderTransform, non-overlapping', (WidgetTester tester) async {

      final List<Widget> children = <Widget>[
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
446
          rect: const Rect.fromLTWH(140, 100, 140, 150),
447
          child: SizedBox(
448 449 450 451 452 453 454 455 456 457 458 459
            width: 300,
            height: 300,
            child: Transform.rotate(
              angle: math.pi / 180 * 15,
              child: const Material(
                elevation: 3.0,
                color: Colors.brown,
              ),
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
460
          rect: const Rect.fromLTWH(50, 50, 100, 100),
461
          child: const SizedBox(
462 463
            width: 300,
            height: 300,
464
            child: Material(
465 466 467 468
              elevation: 2.0,
              color: Colors.red,
              shape: CircleBorder(),
            ),
469 470 471 472 473 474
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 0);
      expect(find.byType(Material), findsNWidgets(2));
475
    }, skip: isBrowser);  // https://github.com/flutter/flutter/issues/52855
476 477 478 479 480 481 482 483 484 485 486 487

    // Tests:
    //
    //   ──────────────                  (brown rect, paints first, rotated so it overlaps)
    //         │      ────────────       (red circle, paints second)
    //         │           │
    //         │           │
    // ────────────────────────────
    // This would be fine without the rotation.
    testWidgets('with a RenderTransform, overlapping', (WidgetTester tester) async {
      final List<Widget> children = <Widget>[
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
488
          rect: const Rect.fromLTWH(140, 100, 140, 150),
489
          child: SizedBox(
490 491 492 493 494 495 496 497 498 499 500 501
            width: 300,
            height: 300,
            child: Transform.rotate(
              angle: math.pi / 180 * 8,
              child: const Material(
                elevation: 3.0,
                color: Colors.brown,
              ),
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
502
          rect: const Rect.fromLTWH(50, 50, 100, 100),
503
          child: const SizedBox(
504 505
            width: 300,
            height: 300,
506
            child: Material(
507 508 509 510
              elevation: 2.0,
              color: Colors.red,
              shape: CircleBorder(),
            ),
511 512 513 514 515 516 517 518
          ),
        ),
      ];

      await _testStackChildren(tester, children, expectedErrorCount: 1);
      expect(find.byType(Material), findsNWidgets(2));
    });
  });
519
}