Add web engine screenshot (scuba) tests (flutter/engine#12353)

* Import all golden files and tests from internal repo.
* Adapt test files to new screenshot API, and tweak some settings.

(Check PR to see individual changes to each file)

Fixes https://github.com/flutter/flutter/issues/40975
This commit is contained in:
David Iglesias 2019-09-20 09:00:55 -07:00 committed by GitHub
parent f6081ed304
commit 0c2c7cfd15
12 changed files with 2137 additions and 3 deletions

View File

@ -49,7 +49,7 @@ import 'goldens.dart';
const int _kChromeDevtoolsPort = 12345;
const int _kMaxScreenshotWidth = 1024;
const int _kMaxScreenshotHeight = 1024;
const double _kMaxDiffRateFailure = 1.0/100000; // 0.001%
const double _kMaxDiffRateFailure = 0.211/100; // 0.211%
class BrowserPlatform extends PlatformPlugin {
/// Starts the server.

View File

@ -0,0 +1,86 @@
// 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 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:test/test.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() async {
final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding
BitmapCanvas canvas;
final PaintData niceRRectPaint = PaintData()
..color = const Color.fromRGBO(250, 186, 218, 1.0) // #fabada
..style = PaintingStyle.fill;
// Some values to see how the algo behaves as radius get absurdly large
const List<double> rRectRadii = <double>[0, 10, 20, 80, 8000];
const Radius someFixedRadius = Radius.circular(10);
setUp(() {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 500, 100));
canvas.translate(10, 10); // Center
});
tearDown(() {
canvas.rootElement.remove();
});
test('round square with big (equal) radius ends up as a circle', () async {
for (int i = 0; i < 5; i++) {
canvas.drawRRect(
RRect.fromRectAndRadius(Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
Radius.circular(rRectRadii[i])),
niceRRectPaint);
}
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/canvas_rrect_round_square.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('round rect with big radius scale down smaller radius', () async {
for (int i = 0; i < 5; i++) {
final Radius growingRadius = Radius.circular(rRectRadii[i]);
final RRect rrect = RRect.fromRectAndCorners(
Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
bottomRight: someFixedRadius,
topRight: growingRadius,
bottomLeft: growingRadius);
canvas.drawRRect(rrect, niceRRectPaint);
}
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/canvas_rrect_overlapping_radius.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('diff round rect with big radius scale down smaller radius', () async {
for (int i = 0; i < 5; i++) {
final Radius growingRadius = Radius.circular(rRectRadii[i]);
final RRect outerRRect = RRect.fromRectAndCorners(
Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
bottomRight: someFixedRadius,
topRight: growingRadius,
bottomLeft: growingRadius);
// Inner is half of outer, but offset a little so it looks nicer
final RRect innerRRect = RRect.fromRectAndCorners(
Rect.fromLTWH(100 * i.toDouble() + 5, 5, 40, 40),
bottomRight: someFixedRadius / 2,
topRight: growingRadius / 2,
bottomLeft: growingRadius / 2);
canvas.drawDRRect(outerRRect, innerRRect, niceRRectPaint);
}
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/canvas_drrect_overlapping_radius.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
}

View File

@ -0,0 +1,112 @@
// 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 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:test/test.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() async {
final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding
BitmapCanvas canvas;
setUp(() {
html.document.body.style.transform = 'translate(10px, 10px)';
});
tearDown(() {
html.document.body.style.transform = 'none';
canvas.rootElement.remove();
});
/// Draws several lines, some aligned precisely with the pixel grid, and some
/// that are offset by 0.5 vertically or horizontally.
///
/// The produced picture stresses the antialiasing generated by the browser
/// when positioning and rasterizing `<canvas>` tags. Aliasing artifacts can
/// be seen depending on pixel alignment and whether antialiasing happens
/// before or after rasterization.
void drawMisalignedLines(BitmapCanvas canvas) {
final PaintData linePaint = (Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1)
.webOnlyPaintData;
final PaintData fillPaint =
(Paint()..style = PaintingStyle.fill).webOnlyPaintData;
canvas.drawRect(
const Rect.fromLTWH(0, 0, 40, 40),
linePaint,
);
canvas.drawLine(
const Offset(10, 0),
const Offset(10, 40),
linePaint,
);
canvas.drawLine(
const Offset(20.5, 0),
const Offset(20, 40),
linePaint,
);
canvas.drawCircle(const Offset(30, 10), 3, fillPaint);
canvas.drawCircle(const Offset(30.5, 30), 3, fillPaint);
}
test('renders pixels that are not aligned inside the canvas', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 60, 60));
drawMisalignedLines(canvas);
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/misaligned_pixels_in_canvas_test.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('compensates for misalignment of the canvas', () async {
// Notice the 0.5 offset in the bounds rectangle. It's what causes the
// misalignment of canvas relative to the pixel grid. BitmapCanvas will
// shift its position back to 0.0 and at the same time it will it will
// compensate by shifting the contents of the canvas in the opposite
// direction.
canvas = BitmapCanvas(const Rect.fromLTWH(0.5, 0.5, 60, 60));
drawMisalignedLines(canvas);
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/misaligned_canvas_test.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('fill the whole canvas with color even when transformed', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50));
canvas.translate(25, 25);
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src);
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/bitmap_canvas_fills_color_when_transformed.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('fill the whole canvas with paint even when transformed', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50));
canvas.translate(25, 25);
canvas.drawPaint(PaintData()
..color = const Color.fromRGBO(0, 255, 0, 1.0)
..style = PaintingStyle.fill);
html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/bitmap_canvas_fills_paint_when_transformed.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
}

View File

@ -0,0 +1,542 @@
// 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 'dart:html' as html;
import 'dart:math' as math;
import 'package:ui/ui.dart';
import 'package:ui/src/engine.dart';
import 'package:test/test.dart';
import '../../matchers.dart';
import 'package:web_engine_tester/golden_tester.dart';
final Rect region = Rect.fromLTWH(0, 0, 500, 100);
void main() async {
debugShowClipLayers = true;
setUp(() {
SceneBuilder.debugForgetFrameScene();
for (html.Node scene in html.document.querySelectorAll('flt-scene')) {
scene.remove();
}
});
test('pushClipRect', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRect(
const Rect.fromLTRB(10, 10, 60, 60),
);
_drawTestPicture(builder);
builder.pop();
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_shifted_clip_rect.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('pushClipRect with offset and transform', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 60);
builder.pushTransform(
Matrix4.diagonal3Values(1, -1, 1).storage,
);
builder.pushClipRect(
const Rect.fromLTRB(10, 10, 60, 60),
);
_drawTestPicture(builder);
builder.pop();
builder.pop();
builder.pop();
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_clip_rect_with_offset_and_transform.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('pushClipRRect', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRRect(
RRect.fromLTRBR(10, 10, 60, 60, const Radius.circular(5)),
);
_drawTestPicture(builder);
builder.pop();
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_shifted_clip_rrect.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
test('pushPhysicalShape', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushPhysicalShape(
path: Path()..addRect(const Rect.fromLTRB(10, 10, 60, 60)),
clipBehavior: Clip.hardEdge,
color: const Color.fromRGBO(0, 0, 0, 0.3),
elevation: 0,
);
_drawTestPicture(builder);
builder.pop();
builder.pushOffset(70, 0);
builder.pushPhysicalShape(
path: Path()
..addRRect(RRect.fromLTRBR(10, 10, 60, 60, const Radius.circular(5))),
clipBehavior: Clip.hardEdge,
color: const Color.fromRGBO(0, 0, 0, 0.3),
elevation: 0,
);
_drawTestPicture(builder);
builder.pop();
builder.pop();
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_shifted_physical_shape_clip.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
group('Cull rect computation', () {
_testCullRectComputation();
});
}
void _testCullRectComputation() {
// Draw a picture larger that screen. Verify that cull rect is equal to screen
// bounds.
test('fills screen bounds', () async {
final SceneBuilder builder = SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
Offset.zero, 10000, Paint()..style = PaintingStyle.fill);
});
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, 0, 500, 100));
}, skip: '''TODO(https://github.com/flutter/flutter/issues/40395)
Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500''');
// Draw a picture that overflows the screen. Verify that cull rect is the
// intersection of screen bounds and paint bounds.
test('intersects with screen bounds', () async {
final SceneBuilder builder = SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
});
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, 0, 20, 20));
});
// Draw a picture that's fully outside the screen bounds. Verify the cull rect
// is zero.
test('fully outside screen bounds', () async {
final SceneBuilder builder = SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
const Offset(-100, -100), 20, Paint()..style = PaintingStyle.fill);
});
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, Rect.zero);
expect(picture.debugExactGlobalCullRect, Rect.zero);
});
// Draw a picture that's fully inside the screen. Verify that cull rect is
// equal to the paint bounds.
test('limits to paint bounds if no clip layers', () async {
final SceneBuilder builder = SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
const Offset(50, 50), 10, Paint()..style = PaintingStyle.fill);
});
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 60, 60));
});
// Draw a picture smaller than the screen. Offset it such that it remains
// fully inside the screen bounds. Verify that cull rect is still just the
// paint bounds.
test('offset does not affect paint bounds', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(10, 10);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
const Offset(50, 50), 10, Paint()..style = PaintingStyle.fill);
});
builder.pop();
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 60, 60));
});
// Draw a picture smaller than the screen. Offset it such that the picture
// overflows screen bounds. Verify that the cull rect is the intersection
// between screen bounds and paint bounds.
test('offset overflows paint bounds', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(0, 90);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
});
builder.pop();
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(
picture.debugExactGlobalCullRect, const Rect.fromLTRB(0, 70, 20, 100));
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(0, -20, 20, 10));
}, skip: '''TODO(https://github.com/flutter/flutter/issues/40395)
Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500''');
// Draw a picture inside a layer clip but fill all available space inside it.
// Verify that the cull rect is equal to the layer clip.
test('fills layer clip rect', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRect(
const Rect.fromLTWH(10, 10, 60, 60),
);
builder.pushClipRect(
const Rect.fromLTWH(40, 40, 60, 60),
);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
Offset.zero, 10000, Paint()..style = PaintingStyle.fill);
});
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_cull_rect_fills_layer_clip.png', region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 70, 70));
}, timeout: const Timeout(Duration(seconds: 10)));
// Draw a picture inside a layer clip but position the picture such that its
// paint bounds overflow the layer clip. Verify that the cull rect is the
// intersection between the layer clip and paint bounds.
test('intersects layer clip rect and paint bounds', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRect(
const Rect.fromLTWH(10, 10, 60, 60),
);
builder.pushClipRect(
const Rect.fromLTWH(40, 40, 60, 60),
);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
const Offset(80, 55), 30, Paint()..style = PaintingStyle.fill);
});
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_cull_rect_intersects_clip_and_paint_bounds.png', region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(50, 40, 70, 70));
}, timeout: const Timeout(Duration(seconds: 10)));
// Draw a picture inside a layer clip that's positioned inside the clip using
// an offset layer. Verify that the cull rect is the intersection between the
// layer clip and the offset paint bounds.
test('offsets picture inside layer clip rect', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRect(
const Rect.fromLTWH(10, 10, 60, 60),
);
builder.pushClipRect(
const Rect.fromLTWH(40, 40, 60, 60),
);
builder.pushOffset(55, 70);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
});
builder.pop(); // pushOffset
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_cull_rect_offset_inside_layer_clip.png', region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect,
const Rect.fromLTRB(-15.0, -20.0, 15.0, 0.0));
}, timeout: const Timeout(Duration(seconds: 10)));
// Draw a picture inside a layer clip that's positioned an offset layer such
// that the picture is push completely outside the clip area. Verify that the
// cull rect is zero.
test('zero intersection with clip', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushClipRect(
const Rect.fromLTWH(10, 10, 60, 60),
);
builder.pushClipRect(
const Rect.fromLTWH(40, 40, 60, 60),
);
builder.pushOffset(100, 50);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
});
builder.pop(); // pushOffset
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
builder.build();
final PersistedStandardPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, Rect.zero);
expect(picture.debugExactGlobalCullRect, Rect.zero);
});
// Draw a picture inside a rotated clip. Verify that the cull rect is big
// enough to fit the rotated clip.
test('rotates clip and the picture', () async {
final SceneBuilder builder = SceneBuilder();
builder.pushOffset(80, 50);
builder.pushTransform(
Matrix4.rotationZ(-math.pi / 4).storage,
);
builder.pushClipRect(
const Rect.fromLTRB(-10, -10, 10, 10),
);
builder.pushTransform(
Matrix4.rotationZ(math.pi / 4).storage,
);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawPaint(Paint()
..color = const Color.fromRGBO(0, 0, 255, 0.6)
..style = PaintingStyle.fill);
canvas.drawRect(
const Rect.fromLTRB(-5, -5, 5, 5),
Paint()
..color = const Color.fromRGBO(0, 255, 0, 1.0)
..style = PaintingStyle.fill,
);
});
builder.pop(); // pushTransform
builder.pop(); // pushClipRect
builder.pop(); // pushTransform
builder.pop(); // pushOffset
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_cull_rect_rotated.png', region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
expect(
picture.optimalLocalCullRect,
within(
distance: 0.05, from: const Rect.fromLTRB(-14.1, -14.1, 14.1, 14.1)),
);
}, timeout: const Timeout(Duration(seconds: 10)));
test('pushClipPath', () async {
final SceneBuilder builder = SceneBuilder();
final Path path = Path();
path..addRect(const Rect.fromLTRB(10, 10, 60, 60));
builder.pushClipPath(
path,
);
_drawTestPicture(builder);
builder.pop();
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_clip_path.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
// Draw a picture inside a rotated clip. Verify that the cull rect is big
// enough to fit the rotated clip.
test('clips correctly when using 3d transforms', () async {
final SceneBuilder builder = SceneBuilder();
final double screenWidth = html.window.innerWidth.toDouble();
final double screenHeight = html.window.innerHeight.toDouble();
final Matrix4 scaleTransform = Matrix4.identity().scaled(0.5, 0.2);
builder.pushTransform(
scaleTransform.storage,
);
builder.pushOffset(400, 200);
builder.pushClipRect(
const Rect.fromLTRB(-200, -200, 200, 200),
);
builder.pushTransform(
Matrix4.rotationY(45.0 * math.pi / 180.0).storage,
);
builder.pushClipRect(
const Rect.fromLTRB(-140, -140, 140, 140),
);
builder.pushTransform(Matrix4.translationValues(0, 0, -50).storage);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawPaint(Paint()
..color = const Color.fromRGBO(0, 0, 255, 0.6)
..style = PaintingStyle.fill);
// Rect will be clipped.
canvas.drawRect(
const Rect.fromLTRB(-150, -150, 150, 150),
Paint()
..color = const Color.fromRGBO(0, 255, 0, 1.0)
..style = PaintingStyle.fill,
);
// Should be outside the clip range.
canvas.drawRect(
const Rect.fromLTRB(-150, -150, -140, -140),
Paint()
..color = const Color.fromARGB(0xE0, 255, 0, 0)
..style = PaintingStyle.fill,
);
canvas.drawRect(
const Rect.fromLTRB(140, -150, 150, -140),
Paint()
..color = const Color.fromARGB(0xE0, 255, 0, 0)
..style = PaintingStyle.fill,
);
canvas.drawRect(
const Rect.fromLTRB(-150, 140, -140, 150),
Paint()
..color = const Color.fromARGB(0xE0, 255, 0, 0)
..style = PaintingStyle.fill,
);
canvas.drawRect(
const Rect.fromLTRB(140, 140, 150, 150),
Paint()
..color = const Color.fromARGB(0xE0, 255, 0, 0)
..style = PaintingStyle.fill,
);
// Should be inside clip range
canvas.drawRect(
const Rect.fromLTRB(-100, -100, -90, -90),
Paint()
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
..style = PaintingStyle.fill,
);
canvas.drawRect(
const Rect.fromLTRB(90, -100, 100, -90),
Paint()
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
..style = PaintingStyle.fill,
);
canvas.drawRect(
const Rect.fromLTRB(-100, 90, -90, 100),
Paint()
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
..style = PaintingStyle.fill,
);
canvas.drawRect(
const Rect.fromLTRB(90, 90, 100, 100),
Paint()
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
..style = PaintingStyle.fill,
);
});
builder.pop(); // pushTransform Z-50
builder.pop(); // pushClipRect
builder.pop(); // pushTransform 3D rotate
builder.pop(); // pushClipRect
builder.pop(); // pushOffset
builder.pop(); // pushTransform scale
html.document.body.append(builder.build().webOnlyRootElement);
await matchGoldenFile('engine/compositing_3d_rotate1.png', region: region);
final PersistedStandardPicture picture = enumeratePictures().single;
// TODO(https://github.com/flutter/flutter/issues/40395):
// Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500.
// expect(
// picture.optimalLocalCullRect,
// within(
// distance: 0.05,
// from: Rect.fromLTRB(
// -140, -140, screenWidth - 360.0, screenHeight + 40.0)),
// );
}, timeout: const Timeout(Duration(seconds: 10)));
}
void _drawTestPicture(SceneBuilder builder) {
final PictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas =
recorder.beginRecording(const Rect.fromLTRB(0, 0, 100, 100));
canvas.drawCircle(
const Offset(10, 10), 10, Paint()..style = PaintingStyle.fill);
canvas.drawCircle(
const Offset(60, 10),
10,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(255, 0, 0, 1));
canvas.drawCircle(
const Offset(10, 60),
10,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 255, 0, 1));
canvas.drawCircle(
const Offset(60, 60),
10,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 0, 255, 1));
final Picture picture = recorder.endRecording();
builder.addPicture(
Offset.zero,
picture,
);
}
typedef PaintCallback = void Function(RecordingCanvas canvas);
void drawWithBitmapCanvas(SceneBuilder builder, PaintCallback callback,
{Rect bounds = Rect.largest}) {
final PictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(bounds);
canvas.debugEnforceArbitraryPaint();
callback(canvas);
final Picture picture = recorder.endRecording();
builder.addPicture(
Offset.zero,
picture,
);
}

View File

@ -0,0 +1,93 @@
// 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 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:test/test.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() async {
final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding
Future<void> testPath(Path path, String scubaFileName) async {
const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 800);
final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds);
final RecordingCanvas canvas = RecordingCanvas(canvasBounds);
Paint paint = Paint()
..color = const Color(0x7F7F7F7F)
..style = PaintingStyle.fill;
canvas.drawPath(path, paint);
paint = Paint()
..strokeWidth = 2.0
..color = const Color(0xFF7F007F)
..style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
html.document.body.append(bitmapCanvas.rootElement);
canvas.apply(bitmapCanvas);
await matchGoldenFile('engine/$scubaFileName.png', region: region);
bitmapCanvas.rootElement.remove();
}
test('render conic with control point horizontal center', () async {
const double yStart = 20;
const Offset p0 = Offset(25, yStart + 25);
const Offset pc = Offset(60, yStart + 150);
const Offset p2 = Offset(100, yStart + 50);
final Path path = Path();
path.moveTo(p0.dx, p0.dy);
path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5);
path.close();
path.moveTo(p0.dx, p0.dy + 200);
path.conicTo(pc.dx, pc.dy + 200, p2.dx, p2.dy + 200, 10);
path.close();
await testPath(path, 'render_conic_1_w10');
}, timeout: const Timeout(Duration(seconds: 10)));
test('render conic with control point left of start point', () async {
const double yStart = 20;
const Offset p0 = Offset(60, yStart + 25);
const Offset pc = Offset(25, yStart + 150);
const Offset p2 = Offset(100, yStart + 50);
final Path path = Path();
path.moveTo(p0.dx, p0.dy);
path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5);
path.close();
path.moveTo(p0.dx, p0.dy + 200);
path.conicTo(pc.dx, pc.dy + 200, p2.dx, p2.dy + 200, 10);
path.close();
await testPath(path, 'render_conic_2_w10');
}, timeout: const Timeout(Duration(seconds: 10)));
test('render conic with control point above start point', () async {
const double yStart = 20;
const Offset p0 = Offset(25, yStart + 125);
const Offset pc = Offset(60, yStart + 50);
const Offset p2 = Offset(100, yStart + 150);
final Path path = Path();
path.moveTo(p0.dx, p0.dy);
path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5);
path.close();
path.moveTo(p0.dx, p0.dy + 200);
path.conicTo(pc.dx, pc.dy + 200, p2.dx, p2.dy + 200, 10);
path.close();
await testPath(path, 'render_conic_2');
}, timeout: const Timeout(Duration(seconds: 10)));
}

View File

@ -0,0 +1,194 @@
// 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 'dart:math' as math;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'scuba.dart';
typedef PaintTest = void Function(RecordingCanvas recordingCanvas);
void main() async {
// Scuba doesn't give us viewport smaller than 472px wide.
final EngineScubaTester scuba = await EngineScubaTester.initialize(
viewportSize: const Size(600, 600),
);
void paintTest(EngineCanvas canvas, PaintTest painter) {
final RecordingCanvas recordingCanvas =
RecordingCanvas(const Rect.fromLTWH(0, 0, 600, 600));
painter(recordingCanvas);
recordingCanvas.apply(canvas);
}
testEachCanvas(
'clips multiline text against rectangle',
(EngineCanvas canvas) {
// [DomCanvas] doesn't support clip commands.
if (canvas is! DomCanvas) {
paintTest(canvas, paintTextWithClipRect);
return scuba.diffCanvasScreenshot(
canvas, 'multiline_text_clipping_rect');
}
return null;
},
);
testEachCanvas(
'clips multiline text against rectangle with transform',
(EngineCanvas canvas) {
// [DomCanvas] doesn't support clip commands.
if (canvas is! DomCanvas) {
paintTest(canvas, paintTextWithClipRectTranslated);
return scuba.diffCanvasScreenshot(
canvas, 'multiline_text_clipping_rect_translate');
}
return null;
},
);
testEachCanvas(
'clips multiline text against round rectangle',
(EngineCanvas canvas) {
// [DomCanvas] doesn't support clip commands.
if (canvas is! DomCanvas) {
paintTest(canvas, paintTextWithClipRoundRect);
return scuba.diffCanvasScreenshot(
canvas, 'multiline_text_clipping_roundrect');
}
return null;
},
);
testEachCanvas(
'clips multiline text against path',
(EngineCanvas canvas) {
// [DomCanvas] doesn't support clip commands.
if (canvas is! DomCanvas) {
paintTest(canvas, paintTextWithClipPath);
return scuba.diffCanvasScreenshot(
canvas, 'multiline_text_clipping_path');
}
return null;
},
);
testEachCanvas(
'clips multiline text against stack of rects',
(EngineCanvas canvas) {
// [DomCanvas] doesn't support clip commands.
if (canvas is! DomCanvas) {
// TODO(flutter_web): https://github.com/flutter/flutter/issues/35086
// This produces the wrong result when using [BitmapCanvas] but without
// the new experimental canvas mode.
paintTest(canvas, paintTextWithClipStack);
return scuba.diffCanvasScreenshot(
canvas, 'multiline_text_clipping_stack1');
}
return null;
},
);
}
const Rect testBounds = Rect.fromLTRB(50, 50, 230, 220);
void drawBackground(RecordingCanvas canvas) {
canvas.drawRect(
testBounds,
Paint()
..style = PaintingStyle.fill
..color = const Color(0xFF9E9E9E));
canvas.drawRect(
testBounds.inflate(-40),
Paint()
..strokeWidth = 1
..style = PaintingStyle.stroke
..color = const Color(0xFF009688));
}
void drawQuickBrownFox(RecordingCanvas canvas) {
canvas.drawParagraph(
paragraph(
'The quick brown fox jumps over the lazy dog',
textStyle: TextStyle(
color: const Color(0xFF000000),
decoration: TextDecoration.none,
fontFamily: 'Arial',
fontSize: 30,
background: Paint()..color = const Color.fromRGBO(50, 255, 50, 1.0),
),
maxWidth: 180,
),
Offset(testBounds.left, testBounds.top));
}
void paintTextWithClipRect(RecordingCanvas canvas) {
drawBackground(canvas);
canvas.clipRect(testBounds.inflate(-40));
drawQuickBrownFox(canvas);
}
void paintTextWithClipRectTranslated(RecordingCanvas canvas) {
drawBackground(canvas);
canvas.clipRect(testBounds.inflate(-40));
canvas.translate(30, 10);
drawQuickBrownFox(canvas);
}
const Color deepOrange = Color(0xFFFF5722);
void paintTextWithClipRoundRect(RecordingCanvas canvas) {
final RRect roundRect = RRect.fromRectAndCorners(testBounds.inflate(-40),
topLeft: Radius.zero,
topRight: const Radius.elliptical(45, 40),
bottomLeft: const Radius.elliptical(50, 40),
bottomRight: const Radius.circular(30));
drawBackground(canvas);
canvas.drawRRect(
roundRect,
Paint()
..color = deepOrange
..style = PaintingStyle.fill);
canvas.clipRRect(roundRect);
drawQuickBrownFox(canvas);
}
void paintTextWithClipPath(RecordingCanvas canvas) {
drawBackground(canvas);
final Path path = Path();
const double delta = 40.0;
final Rect clipBounds = testBounds.inflate(-delta);
final double midX = (clipBounds.left + clipBounds.right) / 2.0;
final double midY = (clipBounds.top + clipBounds.bottom) / 2.0;
path.moveTo(clipBounds.left - delta, midY);
path.quadraticBezierTo(midX, midY, midX, clipBounds.top - delta);
path.quadraticBezierTo(midX, midY, clipBounds.right + delta, midY);
path.quadraticBezierTo(midX, midY, midX, clipBounds.bottom + delta);
path.quadraticBezierTo(midX, midY, clipBounds.left - delta, midY);
path.close();
canvas.drawPath(
path,
Paint()
..color = deepOrange
..style = PaintingStyle.fill);
canvas.clipPath(path);
drawQuickBrownFox(canvas);
}
void paintTextWithClipStack(RecordingCanvas canvas) {
drawBackground(canvas);
final Rect inflatedRect = testBounds.inflate(-40);
canvas.clipRect(inflatedRect);
canvas.rotate(math.pi / 8.0);
canvas.translate(40, -40);
canvas.clipRect(inflatedRect);
canvas.drawRect(
inflatedRect,
Paint()
..color = deepOrange
..style = PaintingStyle.fill);
drawQuickBrownFox(canvas);
}

View File

@ -0,0 +1,187 @@
// 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 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:test/test.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() async {
final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding
Future<void> testPath(Path path, String scubaFileName, {Paint paint}) async {
const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 800);
final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds);
final RecordingCanvas canvas = RecordingCanvas(canvasBounds);
paint ??= Paint()
..color = const Color(0x807F7F7F)
..style = PaintingStyle.fill;
canvas.drawPath(path, paint);
paint = Paint()
..strokeWidth = 2
..color = const Color(0xFFFF0000)
..style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
final html.Element svgElement = pathToSvgElement(path, paint);
html.document.body.append(bitmapCanvas.rootElement);
html.document.body.append(svgElement);
canvas.apply(bitmapCanvas);
await matchGoldenFile('engine/$scubaFileName.png', region: region);
bitmapCanvas.rootElement.remove();
svgElement.remove();
}
tearDown(() {
html.document.body.children.clear();
});
test('render line strokes', () async {
final Path path = Path();
path.moveTo(50, 60);
path.lineTo(200, 300);
await testPath(path, 'svg_stroke_line',
paint: Paint()
..color = const Color(0xFFFF0000)
..strokeWidth = 2.0
..style = PaintingStyle.stroke);
}, timeout: const Timeout(Duration(seconds: 10)));
test('render quad bezier curve', () async {
final Path path = Path();
path.moveTo(50, 60);
path.quadraticBezierTo(200, 60, 50, 200);
await testPath(path, 'svg_quad_bezier');
}, timeout: const Timeout(Duration(seconds: 10)));
test('render cubic curve', () async {
final Path path = Path();
path.moveTo(50, 60);
path.cubicTo(200, 60, -100, -50, 150, 200);
await testPath(path, 'svg_cubic_bezier');
}, timeout: const Timeout(Duration(seconds: 10)));
test('render arcs', () async {
final List<ArcSample> arcs = <ArcSample>[
ArcSample(const Offset(0, 0),
largeArc: false, clockwise: false, distance: 20),
ArcSample(const Offset(200, 0),
largeArc: true, clockwise: false, distance: 20),
ArcSample(const Offset(0, 150),
largeArc: false, clockwise: true, distance: 20),
ArcSample(const Offset(200, 150),
largeArc: true, clockwise: true, distance: 20),
ArcSample(const Offset(0, 300),
largeArc: false, clockwise: false, distance: -20),
ArcSample(const Offset(200, 300),
largeArc: true, clockwise: false, distance: -20),
ArcSample(const Offset(0, 450),
largeArc: false, clockwise: true, distance: -20),
ArcSample(const Offset(200, 450),
largeArc: true, clockwise: true, distance: -20)
];
int sampleIndex = 0;
for (ArcSample sample in arcs) {
++sampleIndex;
final Path path = sample.createPath();
await testPath(path, 'svg_arc_$sampleIndex');
}
}, timeout: const Timeout(Duration(seconds: 10)));
test('render rect', () async {
final Path path = Path();
path.addRect(const Rect.fromLTRB(15, 15, 60, 20));
path.addRect(const Rect.fromLTRB(35, 160, 15, 100));
await testPath(path, 'svg_rect');
}, timeout: const Timeout(Duration(seconds: 10)));
test('render notch', () async {
final Path path = Path();
path.moveTo(0, 0);
path.lineTo(83, 0);
path.quadraticBezierTo(98, 0, 99.97, 7.8);
path.arcToPoint(const Offset(162, 7.8),
radius: const Radius.circular(32),
largeArc: false,
clockwise: false,
rotation: 0);
path.lineTo(200, 7.8);
path.lineTo(200, 80);
path.lineTo(0, 80);
path.lineTo(0, 10);
await testPath(path, 'svg_notch');
}, timeout: const Timeout(Duration(seconds: 10)));
}
html.Element pathToSvgElement(Path path, Paint paint) {
final Rect bounds = path.getBounds();
final StringBuffer sb = StringBuffer();
sb.write(
'<svg viewBox="0 0 ${bounds.right} ${bounds.bottom}" width="${bounds.right}" height="${bounds.bottom}">');
sb.write('<path ');
if (paint.style == PaintingStyle.stroke) {
sb.write('stroke="${paint.color.toCssString()}" ');
sb.write('stroke-width="${paint.strokeWidth}" ');
}
if (paint.style == PaintingStyle.fill) {
sb.write('fill="${paint.color.toCssString()}" ');
}
sb.write('d="');
pathToSvg(path, sb); // This is what we're testing!
sb.write('"></path>');
sb.write('</svg>');
final html.Element svgElement =
html.Element.html(sb.toString(), treeSanitizer: _NullTreeSanitizer());
svgElement.style.transform = 'translate(200px, 0px)';
return svgElement;
}
class _NullTreeSanitizer implements html.NodeTreeSanitizer {
@override
void sanitizeTree(html.Node node) {}
}
class ArcSample {
final Offset offset;
final bool largeArc;
final bool clockwise;
final double distance;
ArcSample(this.offset,
{this.largeArc = false, this.clockwise = false, this.distance = 0});
Path createPath() {
final Offset startP =
Offset(75 - distance + offset.dx, 75 - distance + offset.dy);
final Offset endP =
Offset(75.0 + distance + offset.dx, 75.0 + distance + offset.dy);
final Path path = Path();
path.moveTo(startP.dx, startP.dy);
path.arcToPoint(endP,
rotation: 60,
radius: const Radius.elliptical(40, 60),
largeArc: largeArc,
clockwise: clockwise);
return path;
}
// Returns bounds of start/end point of arc.
Rect getBounds() {
final Offset startP =
Offset(75 - distance + offset.dx, 75 - distance + offset.dy);
final Offset endP =
Offset(75.0 + distance + offset.dx, 75.0 + distance + offset.dy);
return Rect.fromLTRB(startP.dx, startP.dy, endP.dx, endP.dy);
}
}

View File

@ -0,0 +1,470 @@
// 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 'dart:html' as html;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:ui/ui.dart' hide TextStyle;
import 'package:ui/src/engine.dart';
import 'package:test/test.dart';
import '../../matchers.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
final Paint testPaint = Paint()..color = const Color(0xFFFF0000);
// 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) }) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
// Draws the estimated bounds so we can spot the bug in Scuba.
engineCanvas
..save()
..drawRect(
rc.computePaintBounds(),
PaintData()
..color = const Color.fromRGBO(0, 0, 255, 1.0)
..style = PaintingStyle.stroke
..strokeWidth = 1.0,
)
..restore();
rc.apply(engineCanvas);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
sceneElement.append(engineCanvas.rootElement);
html.document.body.append(sceneElement);
await matchGoldenFile('engine/paint_bounds_for_$fileName.png', region: region);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
}
setUp(() async {
debugEmulateFlutterTesterEnvironment = true;
await webOnlyInitializePlatform();
webOnlyFontCollection.debugRegisterTestFonts();
await webOnlyFontCollection.ensureFontsLoaded();
});
test('Empty canvas reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400));
expect(rc.computePaintBounds(), Rect.zero);
await _checkScreenshot(rc, 'empty_canvas');
});
test('Computes paint bounds for draw line', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawLine(const Offset(50, 100), const Offset(120, 140), testPaint);
// The off by one is due to the minimum stroke width of 1.
expect(rc.computePaintBounds(), const Rect.fromLTRB(49, 99, 121, 141));
await _checkScreenshot(rc, 'draw_line');
});
test('Computes paint bounds for draw line when line exceeds limits',
() async {
// Uses max bounds when computing paint bounds
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawLine(const Offset(50, 100), const Offset(screenWidth + 100.0, 140),
testPaint);
// The off by one is due to the minimum stroke width of 1.
expect(rc.computePaintBounds(),
const Rect.fromLTRB(49.0, 99.0, screenWidth, 141.0));
await _checkScreenshot(rc, 'draw_line_exceeding_limits');
});
test('Computes paint bounds for draw rect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_rect');
});
test('Computes paint bounds for draw rect when exceeds limits', () async {
// Uses max bounds when computing paint bounds
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRect(
const Rect.fromLTRB(10, 20, 30 + screenWidth, 40 + screenHeight),
testPaint);
expect(rc.computePaintBounds(),
const Rect.fromLTRB(10, 20, screenWidth, screenHeight));
rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(-200, -100, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(0, 0, 30, 40));
await _checkScreenshot(rc, 'draw_rect_exceeding_limits');
});
test('Computes paint bounds for translate', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.translate(5, 7);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(15, 27, 35, 47));
await _checkScreenshot(rc, 'translate');
});
test('Computes paint bounds for scale', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.scale(2, 2);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(20, 40, 60, 80));
await _checkScreenshot(rc, 'scale');
});
test('Computes paint bounds for rotate', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.rotate(math.pi / 4.0);
rc.drawLine(
const Offset(1, 0), Offset(50 * math.sqrt(2) - 1, 0), testPaint);
// The extra 0.7 is due to stroke width of 1 rotated by 45 degrees.
expect(rc.computePaintBounds(),
within(distance: 0.1, from: const Rect.fromLTRB(0, 0, 50.7, 50.7)));
await _checkScreenshot(rc, 'rotate');
});
test('Computes paint bounds for horizontal skew', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.skew(1.0, 0.0);
rc.drawRect(const Rect.fromLTRB(20, 20, 40, 40), testPaint);
expect(
rc.computePaintBounds(),
within(
distance: 0.1, from: const Rect.fromLTRB(40.0, 20.0, 80.0, 40.0)));
await _checkScreenshot(rc, 'skew_horizontally');
});
test('Computes paint bounds for vertical skew', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.skew(0.0, 1.0);
rc.drawRect(const Rect.fromLTRB(20, 20, 40, 40), testPaint);
expect(
rc.computePaintBounds(),
within(
distance: 0.1, from: const Rect.fromLTRB(20.0, 40.0, 40.0, 80.0)));
await _checkScreenshot(rc, 'skew_vertically');
});
test('Computes paint bounds for a complex transform', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Float64List matrix = Float64List(16);
// translate(210, 220) , scale(2, 3), rotate(math.pi / 4.0)
matrix[0] = 1.4;
matrix[1] = 2.12;
matrix[2] = 0.0;
matrix[3] = 0.0;
matrix[4] = -1.4;
matrix[5] = 2.12;
matrix[6] = 0.0;
matrix[7] = 0.0;
matrix[8] = 0.0;
matrix[9] = 0.0;
matrix[10] = 2.0;
matrix[11] = 0.0;
matrix[12] = 210.0;
matrix[13] = 220.0;
matrix[14] = 0.0;
matrix[15] = 1.0;
rc.transform(matrix);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(),
const Rect.fromLTRB(168.0, 283.6, 224.0, 368.4));
await _checkScreenshot(rc, 'complex_transform');
});
test('drawPaint should cover full size', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawPaint(testPaint);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), screenRect);
await _checkScreenshot(rc, 'draw_paint');
});
test('drawColor should cover full size', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawColor(const Color(0xFFFF0000), BlendMode.multiply);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), screenRect);
await _checkScreenshot(rc, 'draw_color');
});
test('Computes paint bounds for draw oval', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawOval(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_oval');
});
test('Computes paint bounds for draw round rect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTRB(10, 20, 30, 40), const Radius.circular(5.0)),
testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_round_rect');
});
test(
'Computes empty paint bounds when inner rect outside of outer rect for '
'drawDRRect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawDRRect(RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)),
RRect.fromRectAndCorners(const Rect.fromLTRB(1, 2, 3, 4)), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(0, 0, 0, 0));
await _checkScreenshot(rc, 'draw_drrect_empty');
});
test('Computes paint bounds using outer rect for drawDRRect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawDRRect(
RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)),
RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38)),
testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_drrect');
});
test('Computes paint bounds for draw circle', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
expect(
rc.computePaintBounds(), const Rect.fromLTRB(10.0, 10.0, 30.0, 30.0));
rc.drawCircle(const Offset(200, 300), 100.0, testPaint);
expect(
rc.computePaintBounds(), const Rect.fromLTRB(10.0, 10.0, 300.0, 400.0));
await _checkScreenshot(rc, 'draw_circle');
});
test('Computes paint bounds for draw image', () {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawImage(TestImage(), const Offset(50, 100), Paint());
expect(
rc.computePaintBounds(), const Rect.fromLTRB(50.0, 100.0, 70.0, 110.0));
});
test('Computes paint bounds for draw image rect', () {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawImageRect(TestImage(), const Rect.fromLTRB(1, 1, 20, 10),
const Rect.fromLTRB(5, 6, 400, 500), Paint());
expect(
rc.computePaintBounds(), const Rect.fromLTRB(5.0, 6.0, 400.0, 500.0));
});
test('Computes paint bounds for single-line draw paragraph', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Paragraph paragraph = createTestParagraph();
const double textLeft = 5.0;
const double textTop = 7.0;
const double widthConstraint = 300.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
expect(
rc.computePaintBounds(),
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 21.0),
);
await _checkScreenshot(rc, 'draw_paragraph');
});
test('Computes paint bounds for multi-line draw paragraph', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Paragraph paragraph = createTestParagraph();
const double textLeft = 5.0;
const double textTop = 7.0;
const double widthConstraint =
130.0; // do not go lower than the shortest word.
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
expect(
rc.computePaintBounds(),
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 35.0),
);
await _checkScreenshot(rc, 'draw_paragraph_multi_line');
});
test('Should exclude painting outside simple clipRect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
expect(rc.computePaintBounds(), Rect.zero);
rc.drawLine(const Offset(52, 53), const Offset(55, 56), testPaint);
// Extra pixel due to default line length
expect(rc.computePaintBounds(), const Rect.fromLTRB(51, 52, 56, 57));
await _checkScreenshot(rc, 'clip_rect_simple');
});
test('Should include intersection of clipRect and painting', () async {
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawRect(const Rect.fromLTRB(20, 60, 120, 70), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(50, 60, 100, 70));
await _checkScreenshot(rc, 'clip_rect_intersects_paint_left_to_right');
rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawRect(const Rect.fromLTRB(60, 20, 70, 200), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(60, 50, 70, 100));
await _checkScreenshot(rc, 'clip_rect_intersects_paint_top_to_bottom');
});
test('Should intersect rects for multiple clipRect calls', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.scale(2.0, 2.0);
rc.clipRect(const Rect.fromLTRB(30, 30, 45, 45));
rc.drawRect(const Rect.fromLTRB(10, 30, 60, 35), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(60, 60, 90, 70));
await _checkScreenshot(rc, 'clip_rects_intersect');
});
// drawShadow
test('Computes paint bounds for drawShadow', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Path path = Path();
path.addRect(const Rect.fromLTRB(20, 30, 100, 110));
rc.drawShadow(path, const Color(0xFFFF0000), 2.0, true);
expect(
rc.computePaintBounds(), const Rect.fromLTRB(15.0, 27.0, 106.0, 117.0));
await _checkScreenshot(rc, 'path_with_shadow');
});
test('Clip with negative scale reports correct paint bounds', () async {
// The following draws a filled rectangle that occupies the bottom half of
// the canvas. Notice that both the clip and the rectangle are drawn
// forward. What makes them appear at the bottom is the translation and a
// vertical flip via a negative scale. This replicates the Material
// overscroll glow effect at the bottom of a list, where it is drawn upside
// down.
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
rc
..translate(0, 100)
..scale(1, -1)
..clipRect(const Rect.fromLTRB(0, 0, 100, 50))
..drawRect(const Rect.fromLTRB(0, 0, 100, 100), Paint());
expect(
rc.computePaintBounds(), const Rect.fromLTRB(0.0, 50.0, 100.0, 100.0));
await _checkScreenshot(rc, 'scale_negative');
});
test('Clip with a rotation reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
rc
..translate(50, 50)
..rotate(math.pi / 4.0)
..clipRect(const Rect.fromLTWH(-20, -20, 40, 40))
..drawRect(const Rect.fromLTWH(-80, -80, 160, 160), Paint());
expect(
rc.computePaintBounds(),
Rect.fromCircle(center: const Offset(50, 50), radius: 20 * math.sqrt(2)),
);
await _checkScreenshot(rc, 'clip_rect_rotated');
});
test('Rotated line reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
rc
..translate(50, 50)
..rotate(math.pi / 4.0)
..drawLine(const Offset(0, 0), const Offset(20, 20), Paint());
expect(
rc.computePaintBounds(),
within(distance: 0.1, from: const Rect.fromLTRB(34.4, 48.6, 65.6, 79.7)),
);
await _checkScreenshot(rc, 'line_rotated');
});
test('Should support reusing path and reset when drawing into canvas.',
() async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
final Path path = Path();
path.moveTo(3, 0);
path.lineTo(100, 97);
rc.drawPath(
path,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFFFF0000));
path.reset();
path.moveTo(0, 3);
path.lineTo(97, 100);
rc.drawPath(
path,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF00FF00));
await _checkScreenshot(rc, 'reuse_path');
});
test('Should draw RRect after line when beginning new path.', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 200, 400));
rc.save();
rc.translate(50.0, 100.0);
final Path path = Path();
// Draw a vertical small line (caret).
path.moveTo(8, 4);
path.lineTo(8, 24);
// Draw round rect below caret.
path.addRRect(
RRect.fromLTRBR(0.5, 100.5, 80.7, 150.7, const Radius.circular(10)));
rc.drawPath(
path,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000));
await _checkScreenshot(rc, 'path_with_line_and_roundrect');
});
}
class TestImage implements Image {
@override
int get width => 20;
@override
int get height => 10;
@override
Future<ByteData> toByteData(
{ImageByteFormat format = ImageByteFormat.rawRgba}) async {
throw UnsupportedError('Cannot encode test image');
}
@override
String toString() => '[$width\u00D7$height]';
@override
void dispose() {}
}
Paragraph createTestParagraph() {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
fontFamily: 'Ahem',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 14.0,
));
builder.addText('A short sentence.');
return builder.build();
}

View File

@ -0,0 +1,131 @@
// 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 'dart:async';
import 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:test/test.dart';
import 'package:web_engine_tester/golden_tester.dart';
/// Class that controls some details of how screenshotting is made.
///
/// (For Googlers: Not really related with internal Scuba anymore)
class EngineScubaTester {
/// The size of the browser window used in this scuba test.
final ui.Size viewportSize;
EngineScubaTester(this.viewportSize);
static Future<EngineScubaTester> initialize(
{ui.Size viewportSize = const ui.Size(2400, 1800)}) async {
assert(viewportSize != null);
assert(() {
if (viewportSize.width.ceil() != viewportSize.width ||
viewportSize.height.ceil() != viewportSize.height) {
throw Exception(
'Scuba only supports integer screen sizes, but found: $viewportSize');
}
if (viewportSize.width < 472) {
throw Exception('Scuba does not support screen width smaller than 472');
}
return true;
}());
return EngineScubaTester(viewportSize);
}
Future<void> diffScreenshot(String fileName) async {
await matchGoldenFile('engine/$fileName.png', region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height));
}
/// Prepares the DOM and inserts all the necessary nodes, then invokes scuba's
/// screenshot diffing.
///
/// It also cleans up the DOM after itself.
Future<void> diffCanvasScreenshot(
EngineCanvas canvas,
String fileName,
) async {
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
sceneElement.append(canvas.rootElement);
html.document.body.append(sceneElement);
String screenshotName = '${fileName}_${canvas.runtimeType}';
if (TextMeasurementService.enableExperimentalCanvasImplementation) {
screenshotName += '+canvas_measurement';
}
await diffScreenshot(screenshotName);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
}
}
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
/// Runs the given test [body] with each type of canvas.
void testEachCanvas(String description, CanvasTest body) {
const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800);
test('$description (bitmap)', () {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
return body(BitmapCanvas(bounds));
} finally {
TextMeasurementService.clearCache();
}
});
test('$description (bitmap + canvas measurement)', () async {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
TextMeasurementService.enableExperimentalCanvasImplementation = true;
await body(BitmapCanvas(bounds));
} finally {
TextMeasurementService.enableExperimentalCanvasImplementation = false;
TextMeasurementService.clearCache();
}
});
test('$description (dom)', () {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
return body(DomCanvas());
} finally {
TextMeasurementService.clearCache();
}
});
test('$description (houdini)', () {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
return body(HoudiniCanvas(bounds));
} finally {
TextMeasurementService.clearCache();
}
});
}
final ui.TextStyle _defaultTextStyle = ui.TextStyle(
color: const ui.Color(0xFF000000),
fontFamily: 'Arial',
fontSize: 14,
);
ui.Paragraph paragraph(
String text, {
ui.ParagraphStyle paragraphStyle,
ui.TextStyle textStyle,
double maxWidth = double.infinity,
}) {
final ui.ParagraphBuilder builder =
ui.ParagraphBuilder(paragraphStyle ?? ui.ParagraphStyle());
builder.pushStyle(textStyle ?? _defaultTextStyle);
builder.addText(text);
builder.pop();
return builder.build()..layout(ui.ParagraphConstraints(width: maxWidth));
}

View File

@ -0,0 +1,154 @@
// 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 'dart:async';
import 'package:ui/ui.dart';
import 'package:ui/src/engine.dart';
import 'scuba.dart';
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
const String threeLines = 'First\nSecond\nThird';
const String veryLongWithShortPrefix =
'Lorem ipsum dolor\nsit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const String veryLongWithShortSuffix =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.';
const String veryLong =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const String longUnbreakable = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
void main() async {
final EngineScubaTester scuba = await EngineScubaTester.initialize(
viewportSize: const Size(800, 800),
);
final TextStyle warningStyle = TextStyle(
color: const Color(0xFFFF0000),
fontFamily: 'Arial',
fontSize: 10,
);
Paragraph warning(String text) {
return paragraph(text, textStyle: warningStyle);
}
testEachCanvas('maxLines clipping', (EngineCanvas canvas) {
Offset offset = Offset.zero;
Paragraph p;
// All three lines are rendered.
p = paragraph(threeLines);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Only the first two lines are rendered.
p = paragraph(threeLines, paragraphStyle: ParagraphStyle(maxLines: 2));
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The whole text is rendered.
p = paragraph(veryLong, maxWidth: 200);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Only the first two lines are rendered.
p = paragraph(veryLong,
paragraphStyle: ParagraphStyle(maxLines: 2), maxWidth: 200);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
return scuba.diffCanvasScreenshot(canvas, 'text_max_lines');
});
testEachCanvas('maxLines with overflow', (EngineCanvas canvas) {
Offset offset = Offset.zero;
Paragraph p;
// Only the first line is rendered with no ellipsis because the first line
// doesn't overflow.
p = paragraph(
threeLines,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The first two lines are rendered with an ellipsis on the 2nd line.
p = paragraph(
veryLongWithShortPrefix,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Only the first line is rendered with an ellipsis.
if (!TextMeasurementService.enableExperimentalCanvasImplementation) {
// This is now correct with the canvas-based measurement, so we shouldn't
// print the "(wrong)" warning.
p = warning('(wrong)');
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height);
}
p = paragraph(
veryLongWithShortSuffix,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Only the first two lines are rendered and the ellipsis appears on the 2nd
// line.
if (!TextMeasurementService.enableExperimentalCanvasImplementation) {
// This is now correct with the canvas-based measurement, so we shouldn't
// print the "(wrong)" warning.
p = warning('(wrong)');
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height);
}
p = paragraph(
veryLong,
paragraphStyle: ParagraphStyle(maxLines: 2, ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
return scuba.diffCanvasScreenshot(canvas, 'text_max_lines_with_ellipsis');
});
testEachCanvas('long unbreakable text', (EngineCanvas canvas) {
Offset offset = Offset.zero;
Paragraph p;
// The whole line is rendered unbroken when there are no constraints.
p = paragraph(longUnbreakable);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The whole line is rendered with an ellipsis.
p = paragraph(
longUnbreakable,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The text is broken into multiple lines.
p = paragraph(longUnbreakable, maxWidth: 200);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Very narrow constraint (less than one character's width).
p = paragraph('AA', maxWidth: 7);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
return scuba.diffCanvasScreenshot(canvas, 'text_long_unbreakable');
});
}

View File

@ -0,0 +1,165 @@
// 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:ui/ui.dart';
import 'package:ui/src/engine.dart';
import 'scuba.dart';
void main() async {
final EngineScubaTester scuba = await EngineScubaTester.initialize(
viewportSize: const Size(800, 800),
);
void drawLetterAndWordSpacing(EngineCanvas canvas) {
Offset offset = Offset.zero;
for (double spacing = 0; spacing < 15; spacing += 5) {
canvas.drawParagraph(
paragraph('HelloWorld',
textStyle: TextStyle(
color: const Color(0xFF000000),
decoration: TextDecoration.none,
fontFamily: 'Arial',
fontSize: 30,
letterSpacing: spacing)),
offset,
);
offset = offset.translate(0, 40);
}
for (double spacing = 0; spacing < 30; spacing += 10) {
final TextStyle textStyle = TextStyle(
color: const Color(0xFF00FF00),
decoration: TextDecoration.none,
fontFamily: 'Arial',
fontSize: 30,
wordSpacing: spacing);
canvas.drawParagraph(
paragraph('Hello World', textStyle: textStyle, maxWidth: 600),
offset,
);
offset = offset.translate(0, 40);
}
}
testEachCanvas('draws text with letter/word spacing', (EngineCanvas canvas) {
drawLetterAndWordSpacing(canvas);
return scuba.diffCanvasScreenshot(
canvas, 'paint_bounds_for_text_style_letter_spacing');
});
void drawTextDecorationStyle(EngineCanvas canvas) {
final List<TextDecorationStyle> decorationStyles = <TextDecorationStyle>[
TextDecorationStyle.solid,
TextDecorationStyle.double,
TextDecorationStyle.dotted,
TextDecorationStyle.dashed,
TextDecorationStyle.wavy,
];
Offset offset = Offset.zero;
for (TextDecorationStyle decorationStyle in decorationStyles) {
final TextStyle textStyle = TextStyle(
color: const Color.fromRGBO(50, 50, 255, 1.0),
decoration: TextDecoration.underline,
decorationStyle: decorationStyle,
decorationColor: const Color.fromRGBO(50, 50, 50, 1.0),
fontFamily: 'Arial',
fontSize: 30,
);
canvas.drawParagraph(
paragraph('Hello World', textStyle: textStyle, maxWidth: 600),
offset,
);
offset = offset.translate(0, 40);
}
}
testEachCanvas('draws text decoration style', (EngineCanvas canvas) {
drawTextDecorationStyle(canvas);
return scuba.diffCanvasScreenshot(
canvas, 'paint_bounds_for_text_decorationStyle');
});
void drawTextDecoration(EngineCanvas canvas) {
final List<TextDecoration> decorations = <TextDecoration>[
TextDecoration.overline,
TextDecoration.underline,
TextDecoration.combine(<TextDecoration>[
TextDecoration.underline,
TextDecoration.lineThrough
]),
TextDecoration.combine(
<TextDecoration>[TextDecoration.underline, TextDecoration.overline]),
TextDecoration.combine(
<TextDecoration>[TextDecoration.overline, TextDecoration.lineThrough])
];
Offset offset = Offset.zero;
for (TextDecoration decoration in decorations) {
final TextStyle textStyle = TextStyle(
color: const Color.fromRGBO(50, 50, 255, 1.0),
decoration: decoration,
decorationStyle: TextDecorationStyle.solid,
decorationColor: const Color.fromRGBO(255, 160, 0, 1.0),
fontFamily: 'Arial',
fontSize: 20,
);
canvas.drawParagraph(
paragraph(
'Hello World $decoration',
textStyle: textStyle,
maxWidth: 600,
),
offset,
);
offset = offset.translate(0, 40);
}
}
testEachCanvas('draws text decoration', (EngineCanvas canvas) {
drawTextDecoration(canvas);
return scuba.diffCanvasScreenshot(
canvas, 'paint_bounds_for_text_decoration');
});
void drawTextWithBackground(EngineCanvas canvas) {
// Single-line text.
canvas.drawParagraph(
paragraph(
'Hello World',
maxWidth: 600,
textStyle: TextStyle(
color: const Color.fromRGBO(0, 0, 0, 1.0),
background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0),
fontFamily: 'Arial',
fontSize: 30,
),
),
Offset.zero,
);
// Multi-line text.
canvas.drawParagraph(
paragraph(
'Multi line Hello World paragraph',
maxWidth: 200,
textStyle: TextStyle(
color: const Color.fromRGBO(0, 0, 0, 1.0),
background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0),
fontFamily: 'Arial',
fontSize: 30,
),
),
const Offset(0, 40),
);
}
testEachCanvas('draws text with a background', (EngineCanvas canvas) {
drawTextWithBackground(canvas);
return scuba.diffCanvasScreenshot(canvas, 'text_background');
});
}

View File

@ -184,8 +184,8 @@ StreamChannel<dynamic> _connectToIframe(String url, int id) {
_iframes[id] = iframe;
iframe
..src = url
..width = '500'
..height = '500';
..width = '1000'
..height = '1000';
document.body.children.add(iframe);
// Use this to communicate securely with the iframe.