| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| import 'package:ui/src/engine.dart'; |
| import 'package:ui/ui.dart'; |
| |
| import '../html/screenshot.dart'; |
| import '../mock_engine_canvas.dart'; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| void testMain() { |
| debugEmulateFlutterTesterEnvironment = true; |
| setUpStableTestFonts(); |
| |
| late RecordingCanvas underTest; |
| late MockEngineCanvas mockCanvas; |
| const Rect screenRect = Rect.largest; |
| |
| setUp(() { |
| underTest = RecordingCanvas(screenRect); |
| mockCanvas = MockEngineCanvas(); |
| }); |
| |
| group('paragraph bounds', () { |
| Paragraph paragraphForBoundsTest(TextAlign alignment) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'Ahem', |
| fontSize: 20, |
| textAlign: alignment, |
| )); |
| builder.addText('A AAAAA AAA'); |
| return builder.build(); |
| } |
| |
| test('not laid out', () { |
| final Paragraph paragraph = paragraphForBoundsTest(TextAlign.start); |
| underTest.drawParagraph(paragraph, Offset.zero); |
| underTest.endRecording(); |
| expect(underTest.pictureBounds, Rect.zero); |
| }); |
| |
| test('finite width', () { |
| final Paragraph paragraph = paragraphForBoundsTest(TextAlign.start); |
| paragraph.layout(const ParagraphConstraints(width: 110)); |
| underTest.drawParagraph(paragraph, Offset.zero); |
| underTest.endRecording(); |
| expect(paragraph.width, 110); |
| expect(paragraph.height, 60); |
| expect(underTest.pictureBounds, const Rect.fromLTRB(0, 0, 100, 60)); |
| }); |
| |
| test('finite width center-aligned', () { |
| final Paragraph paragraph = paragraphForBoundsTest(TextAlign.center); |
| paragraph.layout(const ParagraphConstraints(width: 110)); |
| underTest.drawParagraph(paragraph, Offset.zero); |
| underTest.endRecording(); |
| expect(paragraph.width, 110); |
| expect(paragraph.height, 60); |
| expect(underTest.pictureBounds, const Rect.fromLTRB(5, 0, 105, 60)); |
| }); |
| |
| test('infinite width', () { |
| final Paragraph paragraph = paragraphForBoundsTest(TextAlign.start); |
| paragraph.layout(const ParagraphConstraints(width: double.infinity)); |
| underTest.drawParagraph(paragraph, Offset.zero); |
| underTest.endRecording(); |
| expect(paragraph.width, double.infinity); |
| expect(paragraph.height, 20); |
| expect(underTest.pictureBounds, const Rect.fromLTRB(0, 0, 220, 20)); |
| }); |
| }); |
| |
| group('drawDRRect', () { |
| final RRect rrect = RRect.fromLTRBR(10, 10, 50, 50, const Radius.circular(3)); |
| final SurfacePaint somePaint = SurfacePaint() |
| ..color = const Color(0xFFFF0000); |
| |
| test('Happy case', () { |
| underTest.drawDRRect(rrect, rrect.deflate(1), somePaint); |
| underTest.endRecording(); |
| underTest.apply(mockCanvas, screenRect); |
| |
| _expectDrawDRRectCall(mockCanvas, <String, dynamic>{ |
| 'path': |
| 'Path(' |
| 'MoveTo(${10.0}, ${47.0}) ' |
| 'LineTo(${10.0}, ${13.0}) ' |
| 'Conic(${10.0}, ${10.0}, ${10.0}, ${13.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${47.0}, ${10.0}) ' |
| 'Conic(${50.0}, ${10.0}, ${10.0}, ${50.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${50.0}, ${47.0}) ' |
| 'Conic(${50.0}, ${50.0}, ${50.0}, ${47.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${13.0}, ${50.0}) ' |
| 'Conic(${10.0}, ${50.0}, ${50.0}, ${10.0}, w = ${0.7071067690849304}) ' |
| 'Close() ' |
| 'MoveTo(${11.0}, ${47.0}) ' |
| 'LineTo(${11.0}, ${13.0}) ' |
| 'Conic(${11.0}, ${11.0}, ${11.0}, ${13.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${47.0}, ${11.0}) ' |
| 'Conic(${49.0}, ${11.0}, ${11.0}, ${49.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${49.0}, ${47.0}) ' |
| 'Conic(${49.0}, ${49.0}, ${49.0}, ${47.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${13.0}, ${49.0}) ' |
| 'Conic(${11.0}, ${49.0}, ${49.0}, ${11.0}, w = ${0.7071067690849304}) ' |
| 'Close()' |
| ')', |
| 'paint': somePaint.paintData, |
| }); |
| }); |
| |
| test('Inner RRect > Outer RRect', () { |
| underTest.drawDRRect(rrect, rrect.inflate(1), somePaint); |
| underTest.endRecording(); |
| underTest.apply(mockCanvas, screenRect); |
| // Expect nothing to be called |
| expect(mockCanvas.methodCallLog.length, equals(1)); |
| expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint'); |
| }); |
| |
| test('Inner RRect not completely inside Outer RRect', () { |
| underTest.drawDRRect( |
| rrect, rrect.deflate(1).shift(const Offset(0.0, 10)), somePaint); |
| underTest.endRecording(); |
| underTest.apply(mockCanvas, screenRect); |
| // Expect nothing to be called |
| expect(mockCanvas.methodCallLog.length, equals(1)); |
| expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint'); |
| }); |
| |
| test('Inner RRect same as Outer RRect', () { |
| underTest.drawDRRect(rrect, rrect, somePaint); |
| underTest.endRecording(); |
| underTest.apply(mockCanvas, screenRect); |
| // Expect nothing to be called |
| expect(mockCanvas.methodCallLog.length, equals(1)); |
| expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint'); |
| }); |
| |
| test('deflated corners in inner RRect get passed through to draw', () { |
| // This comes from github issue #40728 |
| final RRect outer = RRect.fromRectAndCorners( |
| const Rect.fromLTWH(0, 0, 88, 48), |
| topLeft: const Radius.circular(6), |
| bottomLeft: const Radius.circular(6)); |
| final RRect inner = outer.deflate(1); |
| |
| expect(inner.brRadius, equals(Radius.zero)); |
| expect(inner.trRadius, equals(Radius.zero)); |
| |
| underTest.drawDRRect(outer, inner, somePaint); |
| underTest.endRecording(); |
| underTest.apply(mockCanvas, screenRect); |
| |
| // Expect to draw, even when inner has negative radii (which get ignored by canvas) |
| _expectDrawDRRectCall(mockCanvas, <String, dynamic>{ |
| 'path': |
| 'Path(' |
| 'MoveTo(${0.0}, ${42.0}) ' |
| 'LineTo(${0.0}, ${6.0}) ' |
| 'Conic(${0.0}, ${0.0}, ${0.0}, ${6.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${88.0}, ${0.0}) ' |
| 'Conic(${88.0}, ${0.0}, ${0.0}, ${88.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${88.0}, ${48.0}) ' |
| 'Conic(${88.0}, ${48.0}, ${48.0}, ${88.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${6.0}, ${48.0}) ' |
| 'Conic(${0.0}, ${48.0}, ${48.0}, ${0.0}, w = ${0.7071067690849304}) ' |
| 'Close() ' |
| 'MoveTo(${1.0}, ${42.0}) ' |
| 'LineTo(${1.0}, ${6.0}) ' |
| 'Conic(${1.0}, ${1.0}, ${1.0}, ${6.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${87.0}, ${1.0}) ' |
| 'Conic(${87.0}, ${1.0}, ${1.0}, ${87.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${87.0}, ${47.0}) ' |
| 'Conic(${87.0}, ${47.0}, ${47.0}, ${87.0}, w = ${0.7071067690849304}) ' |
| 'LineTo(${6.0}, ${47.0}) ' |
| 'Conic(${1.0}, ${47.0}, ${47.0}, ${1.0}, w = ${0.7071067690849304}) ' |
| 'Close()' |
| ')', |
| 'paint': somePaint.paintData, |
| }); |
| }); |
| |
| test('preserve old scuba test behavior', () { |
| final RRect outer = |
| RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)); |
| final RRect inner = |
| RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38)); |
| |
| underTest.drawDRRect(outer, inner, somePaint); |
| underTest.endRecording(); |
| underTest.apply(mockCanvas, screenRect); |
| |
| _expectDrawDRRectCall(mockCanvas, <String, dynamic>{ |
| 'path': |
| 'Path(' |
| 'MoveTo(${10.0}, ${20.0}) ' |
| 'LineTo(${30.0}, ${20.0}) ' |
| 'LineTo(${30.0}, ${40.0}) ' |
| 'LineTo(${10.0}, ${40.0}) ' |
| 'Close() ' |
| 'MoveTo(${12.0}, ${22.0}) ' |
| 'LineTo(${28.0}, ${22.0}) ' |
| 'LineTo(${28.0}, ${38.0}) ' |
| 'LineTo(${12.0}, ${38.0}) ' |
| 'Close()' |
| ')', |
| 'paint': somePaint.paintData, |
| }); |
| }); |
| }); |
| |
| test('Filters out paint commands outside the clip rect', () { |
| // Outside to the left |
| underTest.drawRect(const Rect.fromLTWH(0.0, 20.0, 10.0, 10.0), SurfacePaint()); |
| |
| // Outside above |
| underTest.drawRect(const Rect.fromLTWH(20.0, 0.0, 10.0, 10.0), SurfacePaint()); |
| |
| // Visible |
| underTest.drawRect(const Rect.fromLTWH(20.0, 20.0, 10.0, 10.0), SurfacePaint()); |
| |
| // Inside the layer clip rect but zero-size |
| underTest.drawRect(const Rect.fromLTRB(20.0, 20.0, 30.0, 20.0), SurfacePaint()); |
| |
| // Inside the layer clip but clipped out by a canvas clip |
| underTest.save(); |
| underTest.clipRect(const Rect.fromLTWH(0, 0, 10, 10), ClipOp.intersect); |
| underTest.drawRect(const Rect.fromLTWH(20.0, 20.0, 10.0, 10.0), SurfacePaint()); |
| underTest.restore(); |
| |
| // Outside to the right |
| underTest.drawRect(const Rect.fromLTWH(40.0, 20.0, 10.0, 10.0), SurfacePaint()); |
| |
| // Outside below |
| underTest.drawRect(const Rect.fromLTWH(20.0, 40.0, 10.0, 10.0), SurfacePaint()); |
| |
| underTest.endRecording(); |
| |
| expect(underTest.debugPaintCommands, hasLength(10)); |
| final PaintDrawRect outsideLeft = underTest.debugPaintCommands[0] as PaintDrawRect; |
| expect(outsideLeft.isClippedOut, isFalse); |
| expect(outsideLeft.leftBound, 0); |
| expect(outsideLeft.topBound, 20); |
| expect(outsideLeft.rightBound, 10); |
| expect(outsideLeft.bottomBound, 30); |
| |
| final PaintDrawRect outsideAbove = underTest.debugPaintCommands[1] as PaintDrawRect; |
| expect(outsideAbove.isClippedOut, isFalse); |
| |
| final PaintDrawRect visible = underTest.debugPaintCommands[2] as PaintDrawRect; |
| expect(visible.isClippedOut, isFalse); |
| |
| final PaintDrawRect zeroSize = underTest.debugPaintCommands[3] as PaintDrawRect; |
| expect(zeroSize.isClippedOut, isTrue); |
| |
| expect(underTest.debugPaintCommands[4], isA<PaintSave>()); |
| |
| final PaintClipRect clip = underTest.debugPaintCommands[5] as PaintClipRect; |
| expect(clip.isClippedOut, isFalse); |
| |
| final PaintDrawRect clippedOut = underTest.debugPaintCommands[6] as PaintDrawRect; |
| expect(clippedOut.isClippedOut, isTrue); |
| |
| expect(underTest.debugPaintCommands[7], isA<PaintRestore>()); |
| |
| final PaintDrawRect outsideRight = underTest.debugPaintCommands[8] as PaintDrawRect; |
| expect(outsideRight.isClippedOut, isFalse); |
| |
| final PaintDrawRect outsideBelow = underTest.debugPaintCommands[9] as PaintDrawRect; |
| expect(outsideBelow.isClippedOut, isFalse); |
| |
| // Give it the entire screen so everything paints. |
| underTest.apply(mockCanvas, screenRect); |
| expect(mockCanvas.methodCallLog, hasLength(11)); |
| expect(mockCanvas.methodCallLog[0].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[1].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[2].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[3].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[4].methodName, 'save'); |
| expect(mockCanvas.methodCallLog[5].methodName, 'clipRect'); |
| expect(mockCanvas.methodCallLog[6].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[7].methodName, 'restore'); |
| expect(mockCanvas.methodCallLog[8].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[9].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[10].methodName, 'endOfPaint'); |
| |
| // Clip out a middle region that only contains 'drawRect' |
| mockCanvas.methodCallLog.clear(); |
| underTest.apply(mockCanvas, const Rect.fromLTRB(15, 15, 35, 35)); |
| expect(mockCanvas.methodCallLog, hasLength(4)); |
| expect(mockCanvas.methodCallLog[0].methodName, 'drawRect'); |
| expect(mockCanvas.methodCallLog[1].methodName, 'save'); |
| expect(mockCanvas.methodCallLog[2].methodName, 'restore'); |
| expect(mockCanvas.methodCallLog[3].methodName, 'endOfPaint'); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/61697. |
| test('Allows restore calls after recording has ended', () { |
| final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 200, 400)); |
| rc.endRecording(); |
| // Should not throw exception on restore. |
| expect(() => rc.restore(), returnsNormally); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/61697. |
| test('Allows restore calls even if recording is not ended', () { |
| final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 200, 400)); |
| // Should not throw exception on restore. |
| expect(() => rc.restore(), returnsNormally); |
| }); |
| } |
| |
| // Expect a drawDRRect call to be registered in the mock call log, with the expectedArguments |
| void _expectDrawDRRectCall( |
| MockEngineCanvas mock, Map<String, dynamic> expectedArguments) { |
| expect(mock.methodCallLog.length, equals(2)); |
| final MockCanvasCall mockCall = mock.methodCallLog[0]; |
| expect(mockCall.methodName, equals('drawPath')); |
| final Map<String, dynamic> argMap = mockCall.arguments as Map<String, dynamic>; |
| final Map<String, dynamic> argContents = <String, dynamic>{}; |
| argMap.forEach((String key, dynamic value) { |
| argContents[key] = value is SurfacePath ? value.toString() : value; |
| }); |
| expect(argContents, equals(expectedArguments)); |
| } |