decoration_test.dart 25 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:async';
6
import 'dart:ui' as ui show ColorFilter, Image;
7

8
import 'package:fake_async/fake_async.dart';
9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/painting.dart';
11
import 'package:flutter_test/flutter_test.dart';
12

13
import '../image_data.dart';
14 15
import '../painting/mocks_for_image_cache.dart';
import '../rendering/rendering_tester.dart';
16 17

class TestCanvas implements Canvas {
18
  final List<Invocation> invocations = <Invocation>[];
19

20
  @override
21
  void noSuchMethod(Invocation invocation) {
22
    invocations.add(invocation);
23
  }
24 25 26
}

class SynchronousTestImageProvider extends ImageProvider<int> {
27 28 29 30
  const SynchronousTestImageProvider(this.image);

  final ui.Image image;

31 32
  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
33
    return SynchronousFuture<int>(1);
34 35 36
  }

  @override
37
  ImageStreamCompleter load(int key, DecoderCallback decode) {
38
    return OneFrameImageStreamCompleter(
39
      SynchronousFuture<ImageInfo>(TestImageInfo(key, image: image)),
40 41 42 43
    );
  }
}

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
class SynchronousErrorTestImageProvider extends ImageProvider<int> {
  const SynchronousErrorTestImageProvider(this.throwable);

  final Object throwable;

  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
    throw throwable;
  }

  @override
  ImageStreamCompleter load(int key, DecoderCallback decode) {
    throw throwable;
  }
}

60
class AsyncTestImageProvider extends ImageProvider<int> {
61 62 63 64
  AsyncTestImageProvider(this.image);

  final ui.Image image;

65 66
  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
67
    return Future<int>.value(2);
68 69 70
  }

  @override
71
  ImageStreamCompleter load(int key, DecoderCallback decode) {
72
    return OneFrameImageStreamCompleter(
73
      Future<ImageInfo>.value(TestImageInfo(key, image: image)),
74 75 76
    );
  }
}
77

78
class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
79 80 81 82
  DelayedImageProvider(this.image);

  final ui.Image image;

83
  final Completer<ImageInfo> _completer = Completer<ImageInfo>();
84 85

  @override
86
  Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) {
87
    return SynchronousFuture<DelayedImageProvider>(this);
88 89 90
  }

  @override
91
  ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
92
    return OneFrameImageStreamCompleter(_completer.future);
93 94
  }

95 96
  Future<void> complete() async {
    _completer.complete(ImageInfo(image: image));
97 98 99
  }

  @override
100
  String toString() => '${describeIdentity(this)}()';
101 102
}

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
class MultiFrameImageProvider extends ImageProvider<MultiFrameImageProvider> {
  MultiFrameImageProvider(this.completer);

  final MultiImageCompleter completer;

  @override
  Future<MultiFrameImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<MultiFrameImageProvider>(this);
  }

  @override
  ImageStreamCompleter load(MultiFrameImageProvider key, DecoderCallback decode) {
    return completer;
  }

  @override
  String toString() => '${describeIdentity(this)}()';
}

class MultiImageCompleter extends ImageStreamCompleter {
  void testSetImage(ImageInfo info) {
    setImage(info);
  }
}

128
void main() {
129
  TestRenderingFlutterBinding.ensureInitialized();
130

131
  test('Decoration.lerp()', () {
132 133
    const BoxDecoration a = BoxDecoration(color: Color(0xFFFFFFFF));
    const BoxDecoration b = BoxDecoration(color: Color(0x00000000));
134

135
    BoxDecoration c = Decoration.lerp(a, b, 0.0)! as BoxDecoration;
136
    expect(c.color, equals(a.color));
137

138
    c = Decoration.lerp(a, b, 0.25)! as BoxDecoration;
139
    expect(c.color, equals(Color.lerp(const Color(0xFFFFFFFF), const Color(0x00000000), 0.25)));
140

141
    c = Decoration.lerp(a, b, 1.0)! as BoxDecoration;
142
    expect(c.color, equals(b.color));
143
  });
144

145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
  test('Decoration equality', () {
    const BoxDecoration a = BoxDecoration(
      color: Color(0xFFFFFFFF),
      boxShadow: <BoxShadow>[BoxShadow()],
    );

    const BoxDecoration b = BoxDecoration(
      color: Color(0xFFFFFFFF),
      boxShadow: <BoxShadow>[BoxShadow()],
    );

    expect(a.hashCode, equals(b.hashCode));
    expect(a, equals(b));
  });

160 161 162
  test('BoxDecorationImageListenerSync', () async {
    final ui.Image image = await createTestImage(width: 100, height: 100);
    final ImageProvider imageProvider = SynchronousTestImageProvider(image);
163
    final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
164

165
    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
166
    bool onChangedCalled = false;
167
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
168 169 170
      onChangedCalled = true;
    });

171
    final TestCanvas canvas = TestCanvas();
172
    const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
173 174 175 176 177 178
    boxPainter.paint(canvas, Offset.zero, imageConfiguration);

    // The onChanged callback should not be invoked during the call to boxPainter.paint
    expect(onChangedCalled, equals(false));
  });

179 180
  test('BoxDecorationImageListenerAsync', () async {
    final ui.Image image = await createTestImage(width: 10, height: 10);
181
    FakeAsync().run((FakeAsync async) {
182
      final ImageProvider imageProvider = AsyncTestImageProvider(image);
183
      final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
184

185
      final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
186
      bool onChangedCalled = false;
187
      final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
188 189 190
        onChangedCalled = true;
      });

191
      final TestCanvas canvas = TestCanvas();
192
      const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
193 194 195 196 197 198 199 200
      boxPainter.paint(canvas, Offset.zero, imageConfiguration);

      // The onChanged callback should be invoked asynchronously.
      expect(onChangedCalled, equals(false));
      async.flushMicrotasks();
      expect(onChangedCalled, equals(true));
    });
  });
201

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
  test('BoxDecorationImageListener does not change when image is clone', () async {
    final ui.Image image1 = await createTestImage(width: 10, height: 10, cache: false);
    final ui.Image image2 = await createTestImage(width: 10, height: 10, cache: false);
    final MultiImageCompleter completer = MultiImageCompleter();
    final MultiFrameImageProvider imageProvider = MultiFrameImageProvider(completer);
    final DecorationImage backgroundImage = DecorationImage(image: imageProvider);

    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    bool onChangedCalled = false;
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
      onChangedCalled = true;
    });

    final TestCanvas canvas = TestCanvas();
    const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
    boxPainter.paint(canvas, Offset.zero, imageConfiguration);

    // The onChanged callback should be invoked asynchronously.
    expect(onChangedCalled, equals(false));

    completer.testSetImage(ImageInfo(image: image1.clone()));
    await null;

    expect(onChangedCalled, equals(true));
    onChangedCalled = false;
    completer.testSetImage(ImageInfo(image: image1.clone()));
    await null;

    expect(onChangedCalled, equals(false));

    completer.testSetImage(ImageInfo(image: image2.clone()));
    await null;

    expect(onChangedCalled, equals(true));
  });

238 239
  // Regression test for https://github.com/flutter/flutter/issues/7289.
  // A reference test would be better.
240 241
  test('BoxDecoration backgroundImage clip', () async {
    final ui.Image image = await createTestImage(width: 100, height: 100);
242
    void testDecoration({ BoxShape shape = BoxShape.rectangle, BorderRadius? borderRadius, required bool expectClip }) {
243
      assert(shape != null);
244 245
      FakeAsync().run((FakeAsync async) async {
        final DelayedImageProvider imageProvider = DelayedImageProvider(image);
246
        final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
247

248
        final BoxDecoration boxDecoration = BoxDecoration(
249 250
          shape: shape,
          borderRadius: borderRadius,
251
          image: backgroundImage,
252 253
        );

254
        final TestCanvas canvas = TestCanvas();
255
        const ImageConfiguration imageConfiguration = ImageConfiguration(
256
          size: Size(100.0, 100.0),
257 258
        );
        bool onChangedCalled = false;
259
        final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
260 261 262
          onChangedCalled = true;
        });

263
        // _BoxDecorationPainter._paintDecorationImage() resolves the background
264 265
        // image and adds a listener to the resolved image stream.
        boxPainter.paint(canvas, Offset.zero, imageConfiguration);
266
        await imageProvider.complete();
267 268 269 270 271 272 273

        // Run the listener which calls onChanged() which saves an internal
        // reference to the TestImage.
        async.flushMicrotasks();
        expect(onChangedCalled, isTrue);
        boxPainter.paint(canvas, Offset.zero, imageConfiguration);

Josh Soref's avatar
Josh Soref committed
274
        // We expect a clip to precede the drawImageRect call.
275
        final List<Invocation> commands = canvas.invocations.where((Invocation invocation) {
276 277
          return invocation.memberName == #clipPath || invocation.memberName == #drawImageRect;
        }).toList();
Josh Soref's avatar
Josh Soref committed
278
        if (expectClip) { // We expect a clip to precede the drawImageRect call.
279 280 281 282 283 284 285 286 287 288 289
          expect(commands.length, 2);
          expect(commands[0].memberName, equals(#clipPath));
          expect(commands[1].memberName, equals(#drawImageRect));
        } else {
          expect(commands.length, 1);
          expect(commands[0].memberName, equals(#drawImageRect));
        }
      });
    }

    testDecoration(shape: BoxShape.circle, expectClip: true);
290
    testDecoration(borderRadius: const BorderRadius.all(Radius.circular(16.0)), expectClip: true);
291 292
    testDecoration(expectClip: false);
  });
293

294
  test('DecorationImage test', () async {
295
    const ColorFilter colorFilter = ui.ColorFilter.mode(Color(0xFF00FF00), BlendMode.src);
296
    final ui.Image image = await createTestImage(width: 100, height: 100);
297
    final DecorationImage backgroundImage = DecorationImage(
298
      image: SynchronousTestImageProvider(image),
299 300
      colorFilter: colorFilter,
      fit: BoxFit.contain,
301
      alignment: Alignment.bottomLeft,
Dan Field's avatar
Dan Field committed
302
      centerSlice: const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0),
303
      repeat: ImageRepeat.repeatY,
304 305 306 307
      opacity: 0.5,
      filterQuality: FilterQuality.high,
      invertColors: true,
      isAntiAlias: true,
308 309
    );

310
    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
311
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() { assert(false); });
312
    final TestCanvas canvas = TestCanvas();
313
    boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(size: Size(100.0, 100.0)));
314 315 316 317

    final Invocation call = canvas.invocations.singleWhere((Invocation call) => call.memberName == #drawImageNine);
    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(4));
318
    expect(call.positionalArguments[0], isA<ui.Image>());
Dan Field's avatar
Dan Field committed
319 320
    expect(call.positionalArguments[1], const Rect.fromLTRB(10.0, 20.0, 40.0, 60.0));
    expect(call.positionalArguments[2], const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0));
Dan Field's avatar
Dan Field committed
321
    expect(call.positionalArguments[3], isA<Paint>());
322 323 324 325 326 327 328
    final Paint paint = call.positionalArguments[3] as Paint;
    expect(paint.colorFilter, colorFilter);
    expect(paint.color, const Color(0x7F000000)); // 0.5 opacity
    expect(paint.filterQuality, FilterQuality.high);
    expect(paint.isAntiAlias, true);
    // TODO(craiglabenz): change to true when https://github.com/flutter/flutter/issues/88909 is fixed
    expect(paint.invertColors, !kIsWeb);
329
  });
330

331
  test('DecorationImage with null textDirection configuration should throw Error', () async {
332
    const ColorFilter colorFilter = ui.ColorFilter.mode(Color(0xFF00FF00), BlendMode.src);
333
    final ui.Image image = await createTestImage(width: 100, height: 100);
334
    final DecorationImage backgroundImage = DecorationImage(
335
      image: SynchronousTestImageProvider(image),
336 337 338 339
      colorFilter: colorFilter,
      fit: BoxFit.contain,
      centerSlice: const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0),
      repeat: ImageRepeat.repeatY,
340
      matchTextDirection: true,
341 342 343 344
      scale: 0.5,
      opacity: 0.5,
      invertColors: true,
      isAntiAlias: true,
345
    );
346
    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
347 348 349
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
      assert(false);
    });
350 351
    final TestCanvas canvas = TestCanvas();
    late FlutterError error;
352 353
    try {
      boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(
354 355
        size: Size(100.0, 100.0),
      ));
356 357 358 359 360
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
    expect(error.diagnostics.length, 4);
Dan Field's avatar
Dan Field committed
361 362
    expect(error.diagnostics[2], isA<DiagnosticsProperty<DecorationImage>>());
    expect(error.diagnostics[3], isA<DiagnosticsProperty<ImageConfiguration>>());
363 364
    expect(error.toStringDeep(),
      'FlutterError\n'
365
      '   DecorationImage.matchTextDirection can only be used when a\n'
366 367 368 369
      '   TextDirection is available.\n'
      '   When DecorationImagePainter.paint() was called, there was no text\n'
      '   direction provided in the ImageConfiguration object to match.\n'
      '   The DecorationImage was:\n'
370
      '     DecorationImage(SynchronousTestImageProvider(),\n'
371 372 373 374 375
      '     ColorFilter.mode(Color(0xff00ff00), BlendMode.src),\n'
      '     BoxFit.contain, Alignment.center, centerSlice:\n'
      '     Rect.fromLTRB(10.0, 20.0, 40.0, 60.0), ImageRepeat.repeatY,\n'
      '     match text direction, scale 0.5, opacity 0.5,\n'
      '     FilterQuality.low, invert colors, use anti-aliasing)\n'
376
      '   The ImageConfiguration was:\n'
377
      '     ImageConfiguration(size: Size(100.0, 100.0))\n',
378
    );
379
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87364
380

381
  test('DecorationImage - error listener', () async {
382
    late String exception;
383 384
    final DecorationImage backgroundImage = DecorationImage(
      image: const SynchronousErrorTestImageProvider('threw'),
385
      onError: (dynamic error, StackTrace? stackTrace) {
386
        exception = error as String;
387
      },
388 389 390 391 392 393 394 395 396 397 398 399 400
    );

    backgroundImage.createPainter(() { }).paint(
      TestCanvas(),
      Rect.largest,
      Path(),
      ImageConfiguration.empty,
    );
    // Yield so that the exception callback gets called before we check it.
    await null;
    expect(exception, 'threw');
  });

401 402 403 404 405
  test('BoxDecoration.lerp - shapes', () {
    // We don't lerp the shape, we just switch from one to the other at t=0.5.
    // (Use a ShapeDecoration and ShapeBorder if you want to lerp the shapes...)
    expect(
      BoxDecoration.lerp(
406
        const BoxDecoration(),
407 408 409
        const BoxDecoration(shape: BoxShape.circle),
        -1.0,
      ),
410
      const BoxDecoration(),
411 412 413
    );
    expect(
      BoxDecoration.lerp(
414
        const BoxDecoration(),
415 416 417
        const BoxDecoration(shape: BoxShape.circle),
        0.0,
      ),
418
      const BoxDecoration(),
419 420 421
    );
    expect(
      BoxDecoration.lerp(
422
        const BoxDecoration(),
423 424 425
        const BoxDecoration(shape: BoxShape.circle),
        0.25,
      ),
426
      const BoxDecoration(),
427 428 429
    );
    expect(
      BoxDecoration.lerp(
430
        const BoxDecoration(),
431 432 433
        const BoxDecoration(shape: BoxShape.circle),
        0.75,
      ),
434
      const BoxDecoration(shape: BoxShape.circle),
435 436 437
    );
    expect(
      BoxDecoration.lerp(
438
        const BoxDecoration(),
439 440 441
        const BoxDecoration(shape: BoxShape.circle),
        1.0,
      ),
442
      const BoxDecoration(shape: BoxShape.circle),
443 444 445
    );
    expect(
      BoxDecoration.lerp(
446
        const BoxDecoration(),
447 448 449
        const BoxDecoration(shape: BoxShape.circle),
        2.0,
      ),
450
      const BoxDecoration(shape: BoxShape.circle),
451 452 453 454
    );
  });

  test('BoxDecoration.lerp - gradients', () {
455
    const Gradient gradient = LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xFFFFFFFF) ]);
456 457 458
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
459
        const BoxDecoration(gradient: gradient),
460 461
        -1.0,
      ),
462
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x00FFFFFF) ])),
463 464 465 466
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
467
        const BoxDecoration(gradient: gradient),
468 469
        0.0,
      ),
470
      const BoxDecoration(),
471 472 473 474
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
475
        const BoxDecoration(gradient: gradient),
476 477
        0.25,
      ),
478
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x40FFFFFF) ])),
479 480 481 482
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
483
        const BoxDecoration(gradient: gradient),
484 485
        0.75,
      ),
486
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xBFFFFFFF) ])),
487 488 489 490
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
491
        const BoxDecoration(gradient: gradient),
492 493
        1.0,
      ),
494
      const BoxDecoration(gradient: gradient),
495 496 497 498
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
499
        const BoxDecoration(gradient: gradient),
500 501
        2.0,
      ),
502
      const BoxDecoration(gradient: gradient),
503 504
    );
  });
505 506

  test('Decoration.lerp with unrelated decorations', () {
507 508 509 510
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.0), isA<FlutterLogoDecoration>());
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.25), isA<FlutterLogoDecoration>());
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.75), isA<BoxDecoration>());
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 1.0), isA<BoxDecoration>());
511
  });
512

513
  test('paintImage BoxFit.none scale test', () async {
514
    for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
515
      final TestCanvas canvas = TestCanvas();
516

Dan Field's avatar
Dan Field committed
517
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
518
      final ui.Image image = await createTestImage(width: 100, height: 100);
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: scale,
        alignment: Alignment.bottomRight,
        fit: BoxFit.none,
      );

      const Size imageSize = Size(100.0, 100.0);

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

536
      expect(call.positionalArguments[0], isA<ui.Image>());
537 538 539 540 541 542 543

      // sourceRect should contain all pixels of the source image
      expect(call.positionalArguments[1], Offset.zero & imageSize);

      // Image should be scaled down (divided by scale)
      // and be positioned in the bottom right of the outputRect
      final Size expectedTileSize = imageSize / scale;
544
      final Rect expectedTileRect = Rect.fromPoints(
545 546 547 548 549
        outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
        outputRect.bottomRight,
      );
      expect(call.positionalArguments[2], expectedTileRect);

Dan Field's avatar
Dan Field committed
550
      expect(call.positionalArguments[3], isA<Paint>());
551 552 553
    }
  });

554
  test('paintImage BoxFit.scaleDown scale test', () async {
555
    for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
556
      final TestCanvas canvas = TestCanvas();
557 558

      // container size > scaled image size
Dan Field's avatar
Dan Field committed
559
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
560
      final ui.Image image = await createTestImage(width: 100, height: 100);
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: scale,
        alignment: Alignment.bottomRight,
        fit: BoxFit.scaleDown,
      );

      const Size imageSize = Size(100.0, 100.0);

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

578
      expect(call.positionalArguments[0], isA<ui.Image>());
579 580 581 582 583 584 585

      // sourceRect should contain all pixels of the source image
      expect(call.positionalArguments[1], Offset.zero & imageSize);

      // Image should be scaled down (divided by scale)
      // and be positioned in the bottom right of the outputRect
      final Size expectedTileSize = imageSize / scale;
586
      final Rect expectedTileRect = Rect.fromPoints(
587 588 589 590 591
        outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
        outputRect.bottomRight,
      );
      expect(call.positionalArguments[2], expectedTileRect);

Dan Field's avatar
Dan Field committed
592
      expect(call.positionalArguments[3], isA<Paint>());
593 594 595
    }
  });

596
  test('paintImage BoxFit.scaleDown test', () async {
597
    final TestCanvas canvas = TestCanvas();
598 599

    // container height (20 px) < scaled image height (50 px)
Dan Field's avatar
Dan Field committed
600
    const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 20.0);
601
    final ui.Image image = await createTestImage(width: 100, height: 100);
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618

    paintImage(
      canvas: canvas,
      rect: outputRect,
      image: image,
      scale: 2.0,
      alignment: Alignment.bottomRight,
      fit: BoxFit.scaleDown,
    );

    const Size imageSize = Size(100.0, 100.0);

    final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(4));

619
    expect(call.positionalArguments[0], isA<ui.Image>());
620 621 622 623

    // sourceRect should contain all pixels of the source image
    expect(call.positionalArguments[1], Offset.zero & imageSize);

624
    // Image should be scaled down to fit in height
625 626
    // and be positioned in the bottom right of the outputRect
    const Size expectedTileSize = Size(20.0, 20.0);
627
    final Rect expectedTileRect = Rect.fromPoints(
628 629 630 631 632
      outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
      outputRect.bottomRight,
    );
    expect(call.positionalArguments[2], expectedTileRect);

Dan Field's avatar
Dan Field committed
633
    expect(call.positionalArguments[3], isA<Paint>());
634 635
  });

636
  test('paintImage boxFit, scale and alignment test', () async {
637 638 639 640 641 642 643 644 645 646
    const List<BoxFit> boxFits = <BoxFit>[
      BoxFit.contain,
      BoxFit.cover,
      BoxFit.fitWidth,
      BoxFit.fitWidth,
      BoxFit.fitHeight,
      BoxFit.none,
      BoxFit.scaleDown,
    ];

647
    for (final BoxFit boxFit in boxFits) {
648
      final TestCanvas canvas = TestCanvas();
649

Dan Field's avatar
Dan Field committed
650
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
651
      final ui.Image image = await createTestImage(width: 100, height: 100);
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: 3.0,
        fit: boxFit,
      );

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      // Image should be positioned in the center of the container
667
      // ignore: avoid_dynamic_calls
668 669 670
      expect(call.positionalArguments[2].center, outputRect.center);
    }
  });
671

672 673
  test('DecorationImage scale test', () async {
    final ui.Image image = await createTestImage(width: 100, height: 100);
674
    final DecorationImage backgroundImage = DecorationImage(
675
      image: SynchronousTestImageProvider(image),
676
      scale: 4,
677
      alignment: Alignment.topLeft,
678 679 680 681
    );

    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() { assert(false); });
682
    final TestCanvas canvas = TestCanvas();
683 684 685 686 687
    boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(size: Size(100.0, 100.0)));

    final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);
    // The image should scale down to Size(25.0, 25.0) from Size(100.0, 100.0)
    // considering DecorationImage scale to be 4.0 and Image scale to be 1.0.
688
    // ignore: avoid_dynamic_calls
689 690 691
    expect(call.positionalArguments[2].size, const Size(25.0, 25.0));
    expect(call.positionalArguments[2], const Rect.fromLTRB(0.0, 0.0, 25.0, 25.0));
  });
692 693 694 695 696 697 698

  test('DecorationImagePainter disposes of image when disposed',  () async {
    final ImageProvider provider = MemoryImage(Uint8List.fromList(kTransparentImage));

    final ImageStream stream = provider.resolve(ImageConfiguration.empty);

    final Completer<ImageInfo> infoCompleter = Completer<ImageInfo>();
699
    void listener(ImageInfo image, bool syncCall) {
700 701 702
      assert(!infoCompleter.isCompleted);
      infoCompleter.complete(image);
    }
703
    stream.addListener(ImageStreamListener(listener));
704 705 706 707 708 709 710 711 712 713 714 715 716

    final ImageInfo info = await infoCompleter.future;
    final int baselineRefCount = info.image.debugGetOpenHandleStackTraces()!.length;

    final DecorationImagePainter painter = DecorationImage(image: provider).createPainter(() {});
    final Canvas canvas = TestCanvas();
    painter.paint(canvas, Rect.zero, Path(), ImageConfiguration.empty);

    expect(info.image.debugGetOpenHandleStackTraces()!.length, baselineRefCount + 1);
    painter.dispose();
    expect(info.image.debugGetOpenHandleStackTraces()!.length, baselineRefCount);

    info.dispose();
717
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
718
}