physical_model_test.dart 17.4 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 95
    expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
    expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
96 97
    await expectLater(
      find.byKey(key),
98
      matchesGoldenFile('physical_model_overflow.png'),
99
    );
100
  });
101 102 103 104 105

  group('PhysicalModelLayer checks elevation', () {
    Future<void> _testStackChildren(
      WidgetTester tester,
      List<Widget> children, {
106
      required int expectedErrorCount,
107 108 109 110 111 112 113 114 115 116
      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;
117
      final void Function(FlutterErrorDetails)? oldOnError = FlutterError.onError;
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
      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 {
144 145
      const List<Widget> children = <Widget>[
        SizedBox(
146 147
          width: 300,
          height: 300,
148
          child: Material(
149 150 151 152 153
            elevation: 1.0,
            color: Colors.green,
            child: Material(
              elevation: 2.0,
              color: Colors.red,
154
            ),
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
          ),
        ),
      ];

      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 {
172 173
      const List<Widget> children = <Widget>[
        SizedBox(
174 175
          width: 300,
          height: 300,
176
          child: Material(
177 178 179 180
            elevation: 1.0,
            color: Colors.green,
          ),
        ),
181
        SizedBox(
182 183
          width: 300,
          height: 300,
184
          child: Material(
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
            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 {
204 205
      const List<Widget> children = <Widget>[
        SizedBox(
206 207
          width: 300,
          height: 300,
208
          child: Material(
209 210 211 212
            elevation: 2.0,
            color: Colors.green,
          ),
        ),
213
        SizedBox(
214 215
          width: 300,
          height: 300,
216
          child: Material(
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
            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
239
          rect: const Rect.fromLTWH(150, 150, 150, 150),
240
          child: const SizedBox(
241 242
            width: 300,
            height: 300,
243
            child: Material(
244 245 246 247 248 249
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
250
          rect: const Rect.fromLTWH(20, 20, 140, 150),
251
          child: const SizedBox(
252 253
            width: 300,
            height: 300,
254
            child: Material(
255 256
              elevation: 2.0,
              color: Colors.red,
257
              shape: CircleBorder(),
258 259 260 261 262 263 264
            ),
          ),
        ),
      ];

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

    // 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
277
          rect: const Rect.fromLTWH(20, 20, 140, 150),
278
          child: const SizedBox(
279 280
            width: 300,
            height: 300,
281
            child: Material(
282 283 284 285 286 287
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
288
          rect: const Rect.fromLTWH(50, 50, 100, 100),
289
          child: const SizedBox(
290 291
            width: 300,
            height: 300,
292
            child: Material(
293 294
              elevation: 2.0,
              color: Colors.red,
295
              shape: CircleBorder(),
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
            ),
          ),
        ),
      ];

      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
315
          rect: const Rect.fromLTWH(150, 150, 150, 150),
316
          child: const SizedBox(
317 318
            width: 300,
            height: 300,
319
            child: Material(
320 321 322 323 324 325
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
326
          rect: const Rect.fromLTWH(30, 20, 150, 150),
327
          child: const SizedBox(
328 329
            width: 300,
            height: 300,
330
            child: Material(
331 332
              elevation: 2.0,
              color: Colors.red,
333
              shape: CircleBorder(),
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
            ),
          ),
        ),
      ];

      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
356
          rect: const Rect.fromLTWH(150, 150, 150, 150),
357
          child: const SizedBox(
358 359
            width: 300,
            height: 300,
360
            child: Material(
361 362 363 364 365 366 367 368 369 370 371 372 373
              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
374
          rect: const Rect.fromLTWH(30, 20, 180, 180),
375
          child: const SizedBox(
376 377
            width: 300,
            height: 300,
378
            child: Material(
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
              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 {
398
      final List<Widget> children = <Widget>[
399
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
400
          rect: const Rect.fromLTWH(150, 150, 150, 150),
401
          child: const SizedBox(
402 403
            width: 300,
            height: 300,
404
            child: Material(
405 406 407 408 409 410
              elevation: 3.0,
              color: Colors.brown,
            ),
          ),
        ),
        Positioned.fromRect(
Dan Field's avatar
Dan Field committed
411
          rect: const Rect.fromLTWH(30, 20, 150, 150),
412
          child: const SizedBox(
413 414
            width: 300,
            height: 300,
415
            child: Material(
416 417
              elevation: 2.0,
              color: Colors.red,
418
              shape: CircleBorder(),
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
            ),
          ),
        ),
      ];

      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
444
          rect: const Rect.fromLTWH(140, 100, 140, 150),
445
          child: SizedBox(
446 447 448 449 450 451 452 453 454 455 456 457
            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
458
          rect: const Rect.fromLTWH(50, 50, 100, 100),
459
          child: const SizedBox(
460 461
            width: 300,
            height: 300,
462
            child: Material(
463 464 465 466
              elevation: 2.0,
              color: Colors.red,
              shape: CircleBorder(),
            ),
467 468 469 470 471 472
          ),
        ),
      ];

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

    // 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
486
          rect: const Rect.fromLTWH(140, 100, 140, 150),
487
          child: SizedBox(
488 489 490 491 492 493 494 495 496 497 498 499
            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
500
          rect: const Rect.fromLTWH(50, 50, 100, 100),
501
          child: const SizedBox(
502 503
            width: 300,
            height: 300,
504
            child: Material(
505 506 507 508
              elevation: 2.0,
              color: Colors.red,
              shape: CircleBorder(),
            ),
509 510 511 512 513 514 515 516
          ),
        ),
      ];

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