blob: 9075089ae9b38fec7ef1535abe7963058f0e0a12 [file] [log] [blame]
// 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.
// @dart = 2.6
import 'dart:html' as html;
import 'dart:math' as math;
import 'dart:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart';
import 'package:ui/src/engine.dart';
import 'package:web_engine_tester/golden_tester.dart';
import 'scuba.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
// Commit a recording canvas to a bitmap, and compare with the expected
Future<void> _checkScreenshot(RecordingCanvas rc, String fileName,
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
double maxDiffRatePercent = 0.0, bool setupPerspective = false,
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect,
RenderStrategy());
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
if (setupPerspective) {
// iFrame disables perspective, set it explicitly for test.
engineCanvas.rootElement.style.perspective = '400px';
for (html.Element element in engineCanvas.rootElement.querySelectorAll(
'div')) {
element.style.perspective = '400px';
}
}
sceneElement.append(engineCanvas.rootElement);
html.document.body.append(sceneElement);
await matchGoldenFile('$fileName.png',
region: region, maxDiffRatePercent: maxDiffRatePercent, write: write);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
}
setUp(() async {
debugEmulateFlutterTesterEnvironment = true;
});
setUpStableTestFonts();
test('Paints image', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.drawImage(createTestImage(), Offset(0, 0), new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image');
});
test('Paints image with transform', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.translate(50.0, 100.0);
rc.rotate(math.pi / 4.0);
rc.drawImage(createTestImage(), Offset(0, 0), new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image_with_transform');
});
test('Paints image with transform and offset', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.translate(50.0, 100.0);
rc.rotate(math.pi / 4.0);
rc.drawImage(createTestImage(), Offset(30, 20), new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image_with_transform_and_offset');
});
test('Paints image with transform using destination', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.translate(50.0, 100.0);
rc.rotate(math.pi / 4.0);
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image_rect_with_transform');
});
test('Paints image with source and destination', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.drawImageRect(
testImage,
Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image_rect_with_source');
});
test('Paints image with source and destination and round clip', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.save();
rc.clipRRect(RRect.fromLTRBR(
100, 30, 2 * testWidth, 2 * testHeight, Radius.circular(16)));
rc.drawImageRect(
testImage,
Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image_rect_with_source_and_clip');
});
test('Paints image with transform using source and destination', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.translate(50.0, 100.0);
rc.rotate(math.pi / 6.0);
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.drawImageRect(
testImage,
Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_image_rect_with_transform_source');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should draw on top of image not below.
test('Paints on top of image', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.restore();
await _checkScreenshot(rc, 'draw_circle_on_image');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should below image not on top.
test('Paints below image', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
rc.restore();
await _checkScreenshot(rc, 'draw_circle_below_image');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should draw on top of image with clip rect.
test('Paints on top of image with clip rect', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.clipRect(Rect.fromLTRB(75, 75, 160, 160), ClipOp.intersect);
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.restore();
await _checkScreenshot(rc, 'draw_circle_on_image_clip_rect');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should draw on top of image with clip rect and transform.
test('Paints on top of image with clip rect with transform', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
// Rotate around center of circle.
rc.translate(100, 100);
rc.rotate(math.pi / 4.0);
rc.translate(-100, -100);
rc.clipRect(Rect.fromLTRB(75, 75, 160, 160), ClipOp.intersect);
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.restore();
await _checkScreenshot(rc, 'draw_circle_on_image_clip_rect_with_transform');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should draw on top of image with stack of clip rect and transforms.
test('Paints on top of image with clip rect with stack', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
// Rotate around center of circle.
rc.translate(100, 100);
rc.rotate(-math.pi / 4.0);
rc.save();
rc.translate(-100, -100);
rc.clipRect(Rect.fromLTRB(75, 75, 160, 160), ClipOp.intersect);
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.restore();
rc.restore();
await _checkScreenshot(rc, 'draw_circle_on_image_clip_rect_with_stack');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should draw on top of image with clip rrect.
test('Paints on top of image with clip rrect', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
rc.clipRRect(RRect.fromLTRBR(75, 75, 160, 160, Radius.circular(5)));
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), Paint());
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.restore();
await _checkScreenshot(rc, 'draw_circle_on_image_clip_rrect');
});
// Regression test for https://github.com/flutter/flutter/issues/44845
// Circle should draw on top of image with clip rrect.
test('Paints on top of image with clip path', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
final Path path = Path();
// Triangle.
path.moveTo(118, 57);
path.lineTo(75, 160);
path.lineTo(160, 160);
rc.clipPath(path);
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), Paint());
rc.drawCircle(
Offset(100, 100),
50.0,
Paint()
..strokeWidth = 3
..color = Color.fromARGB(128, 0, 0, 0));
rc.restore();
await _checkScreenshot(rc, 'draw_circle_on_image_clip_path');
});
// Regression test for https://github.com/flutter/flutter/issues/53078
// Verified that Text+Image+Text+Rect+Text composites correctly.
// Yellow text should be behind image and rectangle.
// Cyan text should be above everything.
test('Paints text above and below image', () async {
// Use a non-Ahem font so that text is visible.
debugEmulateFlutterTesterEnvironment = false;
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
Image testImage = createTestImage();
double testWidth = testImage.width.toDouble();
double testHeight = testImage.height.toDouble();
final Color orange = Color(0xFFFF9800);
final Paragraph paragraph1 = createTestParagraph(
'Should be below below below below below',
color: orange);
paragraph1.layout(const ParagraphConstraints(width: 400.0));
rc.drawParagraph(paragraph1, const Offset(20, 100));
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 100, 200, 200), Paint());
rc.drawRect(
Rect.fromLTWH(50, 50, 100, 200),
Paint()
..strokeWidth = 3
..color = Color(0xA0000000));
final Color cyan = Color(0xFF0097A7);
final Paragraph paragraph2 = createTestParagraph(
'Should be above above above above above',
color: cyan);
paragraph2.layout(const ParagraphConstraints(width: 400.0));
rc.drawParagraph(paragraph2, const Offset(20, 150));
rc.restore();
await _checkScreenshot(
rc,
'draw_text_composite_order_below',
maxDiffRatePercent: 1.0,
region: Rect.fromLTWH(0, 0, 350, 300),
);
});
// Creates a picture
test('Paints nine slice image', () async {
Rect region = const Rect.fromLTWH(0, 0, 500, 500);
EnginePictureRecorder recorder = EnginePictureRecorder();
final Canvas canvas = Canvas(recorder, region);
Image testImage = createNineSliceImage();
canvas.clipRect(Rect.fromLTWH(0, 0, 420, 200));
canvas.drawImageNine(testImage, Rect.fromLTWH(20, 20, 20, 20),
Rect.fromLTWH(20, 20, 400, 400), Paint());
Picture picture = recorder.endRecording();
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.addPicture(Offset(0, 0), picture);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
sceneElement.append(builder.build().webOnlyRootElement);
html.document.body.append(sceneElement);
await matchGoldenFile('draw_nine_slice.png',
region: region, maxDiffRatePercent: 0);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
});
// Regression test for https://github.com/flutter/flutter/issues/78068
// Tests for correct behavior when using drawImageNine with a destination
// size that is too small to render the center portion of the original image.
test('Paints nine slice image', () async {
Rect region = const Rect.fromLTWH(0, 0, 100, 100);
EnginePictureRecorder recorder = EnginePictureRecorder();
final Canvas canvas = Canvas(recorder, region);
Image testImage = createNineSliceImage();
canvas.clipRect(Rect.fromLTWH(0, 0, 100, 100));
// The testImage is 60x60 and the center slice is 20x20 so the edges
// of the image are 40x40. Drawing into a destination that is smaller
// than that will not provide enough room to draw the center portion.
canvas.drawImageNine(testImage, Rect.fromLTWH(20, 20, 20, 20),
Rect.fromLTWH(20, 20, 36, 36), Paint());
Picture picture = recorder.endRecording();
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.addPicture(Offset(0, 0), picture);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
sceneElement.append(builder.build().webOnlyRootElement);
html.document.body.append(sceneElement);
await matchGoldenFile('draw_nine_slice_empty_center.png',
region: region, maxDiffRatePercent: 0);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
});
// Regression test for https://github.com/flutter/flutter/issues/61691
//
// The bug in bitmap_canvas.dart was that when we transformed and clipped
// the image we did not apply `transform-origin: 0 0 0` to the clipping
// element which resulted in an undesirable offset.
test('Paints clipped and transformed image', () async {
final Rect region = const Rect.fromLTRB(0, 0, 60, 70);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.translate(10, 10);
canvas.transform(Matrix4.rotationZ(0.4).storage);
canvas.clipPath(Path()
..moveTo(10, 10)
..lineTo(50, 10)
..lineTo(50, 30)
..lineTo(10, 30)
..close());
canvas.drawImage(createNineSliceImage(), Offset.zero, Paint());
await _checkScreenshot(canvas, 'draw_clipped_and_transformed_image',
region: region, maxDiffRatePercent: 1.0);
});
/// Regression test for https://github.com/flutter/flutter/issues/61245
test('Should render image with perspective', () async {
final Rect region = const Rect.fromLTRB(0, 0, 200, 200);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.translate(10, 10);
canvas.drawImage(createTestImage(), Offset(0, 0), new Paint());
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.0005); // perspective
canvas.transform(transform.storage);
canvas.drawImage(createTestImage(), Offset(0, 100), new Paint());
await _checkScreenshot(canvas, 'draw_3d_image',
region: region,
maxDiffRatePercent: 6.0,
setupPerspective: true);
});
/// Regression test for https://github.com/flutter/flutter/issues/61245
test('Should render image with perspective inside clip area', () async {
final Rect region = const Rect.fromLTRB(0, 0, 200, 200);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0));
canvas.translate(10, 10);
canvas.drawImage(createTestImage(), Offset(0, 0), new Paint());
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.0005); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 200), Paint()..color = Color(0x801080E0));
canvas.drawImage(createTestImage(), Offset(0, 100), new Paint());
canvas.drawRect(Rect.fromLTWH(50, 150, 50, 20), Paint()..color = Color(0x80000000));
await _checkScreenshot(canvas, 'draw_3d_image_clipped',
region: region,
maxDiffRatePercent: 5.0,
setupPerspective: true);
});
test('Should render rect with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0));
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(Rect.fromLTWH(0, 60, 120, 40), Paint()..color = Color(0x801080E0));
canvas.drawRect(Rect.fromLTWH(300, 250, 120, 40), Paint()..color = Color(0x80E010E0));
canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(0, 120, 160, 40), Radius.circular(5)),
Paint()..color = Color(0x801080E0));
canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(300, 320, 90, 40), Radius.circular(20)),
Paint()..color = Color(0x80E010E0));
await _checkScreenshot(canvas, 'draw_3d_rect_clipped',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
test('Should render color and ovals with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFFF0000));
canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawOval(Rect.fromLTWH(0, 120, 130, 40),
Paint()..color = Color(0x801080E0));
canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40),
Paint()..color = Color(0x80E010E0));
canvas.drawCircle(Offset(60, 240), 50, Paint()..color = Color(0x801080E0));
canvas.drawCircle(Offset(360, 370), 30, Paint()..color = Color(0x80E010E0));
await _checkScreenshot(canvas, 'draw_3d_oval_clipped',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
test('Should render path with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFFF0000));
canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40),
Paint()..color = Color(0x801080E0));
canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40),
Paint()..color = Color(0x80E010E0));
Path path = Path();
path.moveTo(50, 50);
path.lineTo(100, 50);
path.lineTo(100, 100);
path.close();
canvas.drawPath(path, Paint()..color = Color(0x801080E0));
canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000));
await _checkScreenshot(canvas, 'draw_3d_path',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
test('Should render path with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFFF0000));
canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
//canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40),
Paint()..color = Color(0x801080E0));
canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40),
Paint()..color = Color(0x80E010E0));
Path path = Path();
path.moveTo(50, 50);
path.lineTo(100, 50);
path.lineTo(100, 100);
path.close();
canvas.drawPath(path, Paint()..color = Color(0x801080E0));
canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000));
await _checkScreenshot(canvas, 'draw_3d_path_clipped',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
}
// 9 slice test image that has a shiny/glass look.
const String base64ImageData = 'data:image/png;base64,iVBORw0KGgoAAAANSUh'
'EUgAAADwAAAA8CAYAAAA6/NlyAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPo'
'AAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAApGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQA'
'AARoABQAAAAEAAABKARsABQAAAAEAAABSATEAAgAAACAAAABah2kABAAAAAEAAAB6AAAAAAAA'
'AEgAAAABAAAASAAAAAFBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpAAADoAEAAwAA'
'AAEAAQAAoAIABAAAAAEAAAA8oAMABAAAAAEAAAA8AAAAAKgRPeEAAAAJcEhZcwAACxMAAAs'
'TAQCanBgAAATqaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOn'
'g9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+CiAgIDxyZGY6Uk'
'RGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW'
'5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAg'
'IHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgICA'
'gICAgICB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1J'
'lc291cmNlUmVmIyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlL'
'mNvbS9leGlmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2'
'JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmF'
'kb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAua'
'WlkOjMxRTc0MTc5ODQwQTExRUE5OEU4QUI4OTRCMjhDRUE3PC94bXBNTTpJbnN0YW5jZUl'
'EPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOjMxRTc0MTdBODQwQTExR'
'UE5OEU4QUI4OTRCMjhDRUE3PC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU0'
'6RGVyaXZlZEZyb20gcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICA8c'
'3RSZWY6aW5zdGFuY2VJRD54bXAuZGlkOjAxODAxMTc0MDcyMDY4MTE4MjJBQUI1NDhBQTA'
'zMDNBPC9zdFJlZjppbnN0YW5jZUlEPgogICAgICAgICAgICA8c3RSZWY6ZG9jdW1lbnRJR'
'D54bXAuZGlkOjAxODAxMTc0MDcyMDY4MTE4MjJBQUI1NDhBQTAzMDNBPC9zdFJlZjpkb2N'
'1bWVudElEPgogICAgICAgICA8L3htcE1NOkRlcml2ZWRGcm9tPgogICAgICAgICA8ZXhpZ'
'jpQaXhlbFlEaW1lbnNpb24+NjA8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8'
'ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl'
'4ZWxYRGltZW5zaW9uPjYwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPHhtcD'
'pDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpPC94bXA6Q3Jl'
'YXRvclRvb2w+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YX'
'Rpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZ'
'XRhPgpq1fpCAAAUDUlEQVRoBd1beYxd11n/3fvu2+fNm9VbPB6PHS+ldt0kNKWJk0YkaVJS'
'UtRQhFCqSggRCVWgSKUKEgi1FNSKlvIH/1dVoWqatBKkUYqgAVRaNSTN0jg2buIkE4/XmbF'
'ne/O2u/D7ffedN2/GM9hJCRAf+76z3LN8+/nuOd94jUYjwRWmJFnp6nkeVI+jCJ7vw+Mc9l'
'Z9+M7qLK+MYHPPOmrvjulp17yunxvr+inXWmvh6Bl+2WJw2R7qwJUFehLHiIUEH58LWxIAa'
'RerOoBi1Qi8S71AujaXW69O314k3XvXprURhrZ+ijyJ45HYIlLPWm7cevkVIUw+GieThFzN'
'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'
'7T81zbIITGpJk9OwtIVbZ'
'/dNp0M3036d0iP91QvNopo9Ukg3P8v2wWAv6CuXaIz4TR9OMoaaXtXYZx7EjusY7FIYRJkI'
'Z/XBQJV1qYuwsZsvyuU+9FWXMRBuw+77fwuvV4ex8O1vGY4Bg1kUkp8wfkLy7dH39hW84hE'
'KKrhCfB2atgDFz074VaFYbpgkMcJI/V1im7Z4kyb9rQPjtuRTh5QoT1sMjVDCLTVcpN/w2qt'
'kvI+df/QQdrz/RnJ2AGXaozIZmMsytkvi3Jm3i7omli6X+PU0PDBkccnJNWMIPn4fzk3swpn'
'vfRf1I0eM4hokAvtFXqLn6aRkeDhGZ0VzWFwIraVO/HVzJ7ETB21AZ9G1meGr+WR9mDS3mC'
'pLpQhbyi2LJDJvDqKFOkMw0vsw9VaYUumee3HNbb/M6NkJDPDIapB/DzHAs7oU2RWZ05Tda'
'Fr3nRuRS8u8YJtnZMDsLAPReJ1aUxzI1CnMv/o65l9/A0tnX0Vy/gKi4ycMOIePANAjgJ'
'cu8pO9VTWe0dxV1/bpnb10TjldirDPO4roHDttciMbEFpxziqu8YxvHMC5aFhcpTMoqEaH'
'BxEf9+KsZLBcqmLsBqEtE4JZN6XGZi2xNjKixcY9sNQgTrDl5o892rTMLQajHHk+5CE8di'
'fDKCVVXgvDwLptJiOcT7N5XTXglLdqmtQ1l+h6EqTYmFjJR0KZc5QlBWhY5F5BjP70bgqnD'
'nPHSVH9zHPrbNEH6LET0HZn37+xUu5xG8DSqp0V7D0ItwVacEikdSjjjJgoqwuGvXNXGOg'
'Z2x0yEXi0PeqFO87AiFGCkemvOKYkSF/6yS1vnLOWTaHN/VcmnSWt0eHThELBHBiCGisGra'
'SI5l6R3qD0q05ZR4TNVHES7bnltEgcg6JF3uVlyFsBpdB+lzgdRTMHeejneZR0H1BrnKECH'
'7GyVy1BAmcsr17WxYs78QNqQF4bppFXqX9BBqIrzmcExwueASjAFzlaWncpqEpJCXVc7wv'
'Nj7eT/BbztCaofk+k0AAAAAElFTkSuQmCC';
HtmlImage createNineSliceImage() {
return HtmlImage(
html.ImageElement()..src = base64ImageData,
60,
60,
);
}
HtmlImage createTestImage({int width = 100, int height = 50}) {
html.CanvasElement canvas =
new html.CanvasElement(width: width, height: height);
html.CanvasRenderingContext2D ctx = canvas.context2D;
ctx.fillStyle = '#E04040';
ctx.fillRect(0, 0, 33, 50);
ctx.fill();
ctx.fillStyle = '#40E080';
ctx.fillRect(33, 0, 33, 50);
ctx.fill();
ctx.fillStyle = '#2040E0';
ctx.fillRect(66, 0, 33, 50);
ctx.fill();
html.ImageElement imageElement = html.ImageElement();
imageElement.src = js_util.callMethod(canvas, 'toDataURL', <dynamic>[]);
return HtmlImage(imageElement, width, height);
}
Paragraph createTestParagraph(String text,
{Color color = const Color(0xFF000000)}) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
fontFamily: 'Roboto',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 14.0,
));
builder.pushStyle(TextStyle(color: color));
builder.addText(text);
return builder.build();
}