[web] Remove HTML from the engine's test suites (#162404)

- Remove html bundles/suites from configs and CI.
- Delete html test files.
- Replace the `isHtml` helper with `false` and refactor accordingly.
This commit is contained in:
Mouad Debbar
2025-02-03 15:01:15 -05:00
committed by GitHub
parent 264bef3cf3
commit 041d5246f7
75 changed files with 686 additions and 13122 deletions

View File

@@ -2,50 +2,6 @@
"_comment": "THIS IS A GENERATED FILE. Do not edit this file directly.",
"_comment2": "See `generate_builder_json.dart` for the generator code",
"builds": [
{
"name": "web_tests/test_bundles/dart2js-html-html",
"drone_dimensions": [
"device_type=none",
"os=Linux"
],
"generators": {
"tasks": [
{
"name": "compile bundle dart2js-html-html",
"parameters": [
"test",
"--compile",
"--bundle=dart2js-html-html"
],
"scripts": [
"flutter/lib/web_ui/dev/felt"
]
}
]
}
},
{
"name": "web_tests/test_bundles/dart2js-html-ui",
"drone_dimensions": [
"device_type=none",
"os=Linux"
],
"generators": {
"tasks": [
{
"name": "compile bundle dart2js-html-ui",
"parameters": [
"test",
"--compile",
"--bundle=dart2js-html-ui"
],
"scripts": [
"flutter/lib/web_ui/dev/felt"
]
}
]
}
},
{
"name": "web_tests/test_bundles/dart2js-canvaskit-engine",
"drone_dimensions": [
@@ -209,8 +165,6 @@
"download_jdk": false
},
"dependencies": [
"web_tests/test_bundles/dart2js-html-html",
"web_tests/test_bundles/dart2js-html-ui",
"web_tests/test_bundles/dart2js-canvaskit-engine",
"web_tests/test_bundles/dart2js-canvaskit-canvaskit",
"web_tests/test_bundles/dart2js-canvaskit-ui",
@@ -234,27 +188,19 @@
"parameters": [
"test",
"--copy-artifacts",
"--suite=chrome-dart2js-html-html",
"--suite=chrome-dart2js-html-ui",
"--suite=chrome-dart2js-canvaskit-engine",
"--suite=chrome-dart2js-canvaskit-canvaskit",
"--suite=chrome-dart2js-canvaskit-ui",
"--suite=chrome-full-dart2js-canvaskit-canvaskit",
"--suite=chrome-full-dart2js-canvaskit-ui",
"--suite=edge-dart2js-html-html",
"--suite=edge-dart2js-html-ui",
"--suite=edge-dart2js-canvaskit-engine",
"--suite=edge-dart2js-canvaskit-canvaskit",
"--suite=edge-dart2js-canvaskit-ui",
"--suite=edge-full-dart2js-canvaskit-canvaskit",
"--suite=edge-full-dart2js-canvaskit-ui",
"--suite=firefox-dart2js-html-html",
"--suite=firefox-dart2js-html-ui",
"--suite=firefox-dart2js-canvaskit-engine",
"--suite=firefox-dart2js-canvaskit-canvaskit",
"--suite=firefox-dart2js-canvaskit-ui",
"--suite=safari-dart2js-html-html",
"--suite=safari-dart2js-html-ui",
"--suite=safari-dart2js-canvaskit-engine",
"--suite=safari-dart2js-canvaskit-canvaskit",
"--suite=safari-dart2js-canvaskit-ui",
@@ -269,24 +215,6 @@
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite chrome-dart2js-html-html",
"parameters": [
"test",
"--run",
"--suite=chrome-dart2js-html-html"
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite chrome-dart2js-html-ui",
"parameters": [
"test",
"--run",
"--suite=chrome-dart2js-html-ui"
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite chrome-dart2js-canvaskit-engine",
"parameters": [
@@ -400,8 +328,6 @@
"download_jdk": false
},
"dependencies": [
"web_tests/test_bundles/dart2js-html-html",
"web_tests/test_bundles/dart2js-html-ui",
"web_tests/test_bundles/dart2js-canvaskit-engine",
"web_tests/test_bundles/dart2js-canvaskit-canvaskit",
"web_tests/test_bundles/dart2js-canvaskit-ui",
@@ -423,27 +349,19 @@
"parameters": [
"test",
"--copy-artifacts",
"--suite=chrome-dart2js-html-html",
"--suite=chrome-dart2js-html-ui",
"--suite=chrome-dart2js-canvaskit-engine",
"--suite=chrome-dart2js-canvaskit-canvaskit",
"--suite=chrome-dart2js-canvaskit-ui",
"--suite=chrome-full-dart2js-canvaskit-canvaskit",
"--suite=chrome-full-dart2js-canvaskit-ui",
"--suite=edge-dart2js-html-html",
"--suite=edge-dart2js-html-ui",
"--suite=edge-dart2js-canvaskit-engine",
"--suite=edge-dart2js-canvaskit-canvaskit",
"--suite=edge-dart2js-canvaskit-ui",
"--suite=edge-full-dart2js-canvaskit-canvaskit",
"--suite=edge-full-dart2js-canvaskit-ui",
"--suite=firefox-dart2js-html-html",
"--suite=firefox-dart2js-html-ui",
"--suite=firefox-dart2js-canvaskit-engine",
"--suite=firefox-dart2js-canvaskit-canvaskit",
"--suite=firefox-dart2js-canvaskit-ui",
"--suite=safari-dart2js-html-html",
"--suite=safari-dart2js-html-ui",
"--suite=safari-dart2js-canvaskit-engine",
"--suite=safari-dart2js-canvaskit-canvaskit",
"--suite=safari-dart2js-canvaskit-ui",
@@ -458,24 +376,6 @@
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite firefox-dart2js-html-html",
"parameters": [
"test",
"--run",
"--suite=firefox-dart2js-html-html"
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite firefox-dart2js-html-ui",
"parameters": [
"test",
"--run",
"--suite=firefox-dart2js-html-ui"
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite firefox-dart2js-canvaskit-engine",
"parameters": [
@@ -527,8 +427,6 @@
"download_jdk": false
},
"dependencies": [
"web_tests/test_bundles/dart2js-html-html",
"web_tests/test_bundles/dart2js-html-ui",
"web_tests/test_bundles/dart2js-canvaskit-engine",
"web_tests/test_bundles/dart2js-canvaskit-canvaskit",
"web_tests/test_bundles/dart2js-canvaskit-ui",
@@ -546,27 +444,19 @@
"parameters": [
"test",
"--copy-artifacts",
"--suite=chrome-dart2js-html-html",
"--suite=chrome-dart2js-html-ui",
"--suite=chrome-dart2js-canvaskit-engine",
"--suite=chrome-dart2js-canvaskit-canvaskit",
"--suite=chrome-dart2js-canvaskit-ui",
"--suite=chrome-full-dart2js-canvaskit-canvaskit",
"--suite=chrome-full-dart2js-canvaskit-ui",
"--suite=edge-dart2js-html-html",
"--suite=edge-dart2js-html-ui",
"--suite=edge-dart2js-canvaskit-engine",
"--suite=edge-dart2js-canvaskit-canvaskit",
"--suite=edge-dart2js-canvaskit-ui",
"--suite=edge-full-dart2js-canvaskit-canvaskit",
"--suite=edge-full-dart2js-canvaskit-ui",
"--suite=firefox-dart2js-html-html",
"--suite=firefox-dart2js-html-ui",
"--suite=firefox-dart2js-canvaskit-engine",
"--suite=firefox-dart2js-canvaskit-canvaskit",
"--suite=firefox-dart2js-canvaskit-ui",
"--suite=safari-dart2js-html-html",
"--suite=safari-dart2js-html-ui",
"--suite=safari-dart2js-canvaskit-engine",
"--suite=safari-dart2js-canvaskit-canvaskit",
"--suite=safari-dart2js-canvaskit-ui",
@@ -581,24 +471,6 @@
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite safari-dart2js-html-html",
"parameters": [
"test",
"--run",
"--suite=safari-dart2js-html-html"
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite safari-dart2js-html-ui",
"parameters": [
"test",
"--run",
"--suite=safari-dart2js-html-ui"
],
"script": "flutter/lib/web_ui/dev/felt"
},
{
"name": "run suite safari-dart2js-canvaskit-engine",
"parameters": [

View File

@@ -7,7 +7,7 @@ import 'package:yaml/yaml.dart';
enum Compiler { dart2js, dart2wasm }
enum Renderer { html, canvaskit, skwasm }
enum Renderer { canvaskit, skwasm }
class CompileConfiguration {
CompileConfiguration(this.name, this.compiler, this.renderer);

View File

@@ -512,3 +512,17 @@ class HtmlPatternMatcher extends Matcher {
return mismatchDescription;
}
}
Matcher listEqual(List<int> source, {int tolerance = 0}) {
return predicate((List<int> target) {
if (source.length != target.length) {
return false;
}
for (int i = 0; i < source.length; i += 1) {
if ((source[i] - target[i]).abs() > tolerance) {
return false;
}
}
return true;
}, source.toString());
}

View File

@@ -10,7 +10,6 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../../common/matchers.dart';
import '../../html/image_test.dart';
void main() {
internalBootstrapBrowserTest(() => doTests);

View File

@@ -1,10 +1,6 @@
# See the `README.md` in this directory for documentation on the structure of
# this file.
compile-configs:
- name: dart2js-html
compiler: dart2js
renderer: html
- name: dart2js-canvaskit
compiler: dart2js
renderer: canvaskit
@@ -30,10 +26,6 @@ test-sets:
- name: canvaskit
directory: canvaskit
# Tests for html-renderer-specific functionality
- name: html
directory: html
# Tests for renderer functionality that can be run on any renderer
- name: ui
directory: ui
@@ -43,14 +35,6 @@ test-sets:
directory: fallbacks
test-bundles:
- name: dart2js-html-html
test-set: html
compile-configs: dart2js-html
- name: dart2js-html-ui
test-set: ui
compile-configs: dart2js-html
- name: dart2js-canvaskit-engine
test-set: engine
compile-configs: dart2js-canvaskit
@@ -114,14 +98,6 @@ run-configs:
canvaskit-variant: full
test-suites:
- name: chrome-dart2js-html-html
test-bundle: dart2js-html-html
run-config: chrome
- name: chrome-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: chrome
- name: chrome-dart2js-canvaskit-engine
test-bundle: dart2js-canvaskit-engine
run-config: chrome
@@ -147,14 +123,6 @@ test-suites:
run-config: chrome-full
artifact-deps: [ canvaskit ]
- name: edge-dart2js-html-html
test-bundle: dart2js-html-html
run-config: edge
- name: edge-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: edge
- name: edge-dart2js-canvaskit-engine
test-bundle: dart2js-canvaskit-engine
run-config: edge
@@ -180,14 +148,6 @@ test-suites:
run-config: edge-full
artifact-deps: [ canvaskit ]
- name: firefox-dart2js-html-html
test-bundle: dart2js-html-html
run-config: firefox
- name: firefox-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: firefox
- name: firefox-dart2js-canvaskit-engine
test-bundle: dart2js-canvaskit-engine
run-config: firefox
@@ -203,14 +163,6 @@ test-suites:
run-config: firefox
artifact-deps: [ canvaskit ]
- name: safari-dart2js-html-html
test-bundle: dart2js-html-html
run-config: safari
- name: safari-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: safari
- name: safari-dart2js-canvaskit-engine
test-bundle: dart2js-canvaskit-engine
run-config: safari

View File

@@ -1,291 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
import '../common/test_initialization.dart';
import 'paragraph/helper.dart';
DomElement get sceneHost =>
EnginePlatformDispatcher.instance.implicitView!.dom.renderingHost.querySelector(
DomManager.sceneHostTagName,
)!;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 500, 100);
late BitmapCanvas canvas;
void appendToScene() {
// Create a <flt-scene> element to make sure our CSS reset applies correctly.
final DomElement testScene = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
testScene.style.position = 'absolute';
testScene.style.transformOrigin = '0 0 0';
testScene.style.transform = 'scale(0.3)';
}
testScene.append(canvas.rootElement);
sceneHost.append(testScene);
}
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
tearDown(() {
sceneHost.querySelector('flt-scene')?.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 SurfacePaintData linePaint =
(SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1)
.paintData;
final SurfacePaintData fillPaint = (SurfacePaint()..style = PaintingStyle.fill).paintData;
canvas.translate(10, 10);
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), RenderStrategy());
drawMisalignedLines(canvas);
appendToScene();
await matchGoldenFile('misaligned_pixels_in_canvas_test.png', region: region);
});
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), RenderStrategy());
canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
drawMisalignedLines(canvas);
appendToScene();
await matchGoldenFile('misaligned_canvas_test.png', region: region);
});
test('fill the whole canvas with color even when transformed', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy());
canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
canvas.translate(25, 25);
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src);
appendToScene();
await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', region: region);
});
test('fill the whole canvas with paint even when transformed', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy());
canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
canvas.translate(25, 25);
canvas.drawPaint(
SurfacePaintData()
..color = const Color.fromRGBO(0, 255, 0, 1.0).value
..style = PaintingStyle.fill,
);
appendToScene();
await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', region: region);
});
// This test reproduces text blurriness when two pieces of text appear inside
// two nested clips:
//
// ┌───────────────────────┐
// │ text in outer clip │
// │ ┌────────────────────┐│
// │ │ text in inner clip ││
// │ └────────────────────┘│
// └───────────────────────┘
//
// This test clips using canvas. See a similar test in `compositing_golden_test.dart`,
// which clips using layers.
//
// More details: https://github.com/flutter/flutter/issues/32274
test('renders clipped DOM text with high quality', () async {
final CanvasParagraph paragraph =
(ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build()
as CanvasParagraph;
paragraph.layout(const ParagraphConstraints(width: 1000));
final Rect canvasSize = Rect.fromLTRB(
0,
0,
paragraph.maxIntrinsicWidth + 16,
2 * paragraph.height + 32,
);
final Rect outerClip = Rect.fromLTRB(0.5, 0.5, canvasSize.right, canvasSize.bottom);
final Rect innerClip = Rect.fromLTRB(
0.5,
canvasSize.bottom / 2 + 0.5,
canvasSize.right,
canvasSize.bottom,
);
canvas = BitmapCanvas(canvasSize, RenderStrategy());
canvas.debugChildOverdraw = true;
canvas.clipRect(outerClip, ClipOp.intersect);
canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
canvas.clipRect(innerClip, ClipOp.intersect);
canvas.drawParagraph(paragraph, Offset(8.5, 8.5 + innerClip.top));
expect(
canvas.rootElement
.querySelectorAll('flt-paragraph')
.map<String>((DomElement e) => e.innerText)
.toList(),
<String>['Am I blurry?', 'Am I blurry?'],
reason: 'Expected to render text using HTML',
);
appendToScene();
await matchGoldenFile('bitmap_canvas_draws_high_quality_text.png', region: canvasSize);
}, testOn: 'chrome');
// NOTE: Chrome in --headless mode does not reproduce the bug that this test
// attempts to reproduce. However, it's still good to have this test
// for potential future regressions related to paint order.
test('draws text on top of canvas when transformed and clipped', () async {
final ParagraphBuilder builder = ParagraphBuilder(
ParagraphStyle(fontFamily: 'Ahem', fontSize: 18),
);
const String text =
'This text is intentionally very long to make sure that it '
'breaks into multiple lines.';
builder.addText(text);
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
paragraph.layout(const ParagraphConstraints(width: 100));
final Rect canvasSize = Offset.zero & const Size(500, 500);
canvas = BitmapCanvas(canvasSize, RenderStrategy());
canvas.debugChildOverdraw = true;
final SurfacePaintData pathPaint =
SurfacePaintData()
..color = 0xFF7F7F7F
..style = PaintingStyle.fill;
const double r = 200.0;
const double l = 50.0;
final Path path = (Path()
..moveTo(-l, -l)
..lineTo(0, -r)
..lineTo(l, -l)
..lineTo(r, 0)
..lineTo(l, l)
..lineTo(0, r)
..lineTo(-l, l)
..lineTo(-r, 0)
..close())
.shift(const Offset(250, 250));
final SurfacePaintData borderPaint =
SurfacePaintData()
..color = black.value
..style = PaintingStyle.stroke;
canvas.drawPath(path, pathPaint);
canvas.drawParagraph(paragraph, const Offset(180, 50));
canvas.drawRect(Rect.fromLTWH(180, 50, paragraph.width, paragraph.height), borderPaint);
expect(
canvas.rootElement
.querySelectorAll('flt-paragraph')
.map<String?>((DomElement e) => e.text)
.toList(),
<String>[text],
reason: 'Expected to render text using HTML',
);
final SceneBuilder sb = SceneBuilder();
sb.pushTransform(
Matrix4.diagonal3Values(
EngineFlutterDisplay.instance.browserDevicePixelRatio,
EngineFlutterDisplay.instance.browserDevicePixelRatio,
1.0,
).toFloat64(),
);
sb.pushTransform(Matrix4.rotationZ(math.pi / 2).toFloat64());
sb.pushOffset(0, -500);
sb.pushClipRect(canvasSize);
sb.pop();
sb.pop();
sb.pop();
sb.pop();
final SurfaceScene scene = sb.build() as SurfaceScene;
final DomElement sceneElement = scene.webOnlyRootElement!;
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
sceneElement.querySelector('flt-clip')!.append(canvas.rootElement);
sceneHost.append(sceneElement);
await matchGoldenFile('bitmap_canvas_draws_text_on_top_of_canvas.png', region: canvasSize);
});
// Regression test for https://github.com/flutter/flutter/issues/96498. When
// a picture is made of just text that can be rendered using plain HTML,
// BitmapCanvas should not create any <canvas> elements as they are expensive.
test('does not allocate bitmap canvas just for text', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy());
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'));
builder.addText('Hello');
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
paragraph.layout(const ParagraphConstraints(width: 1000));
canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
expect(canvas.rootElement.querySelectorAll('canvas'), isEmpty);
expect(canvas.rootElement.querySelectorAll('flt-paragraph').single.innerText, 'Hello');
});
}

View File

@@ -1,211 +0,0 @@
// 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:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' as engine;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../common/test_initialization.dart';
import 'screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
// Regression test for https://github.com/flutter/flutter/issues/48683
// Should clip image with oval.
test('Clips image with oval clip path', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.save();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
final Path path = Path();
path.addOval(Rect.fromLTWH(100, 30, testWidth, testHeight));
rc.clipPath(path);
rc.drawImageRect(
testImage,
Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTWH(100, 30, testWidth, testHeight),
engine.SurfacePaint(),
);
rc.restore();
await canvasScreenshot(rc, 'image_clipped_by_oval');
});
// Regression test for https://github.com/flutter/flutter/issues/48683
test('Clips triangle with oval clip path', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.save();
const double testWidth = 200;
const double testHeight = 150;
final Path path = Path();
path.addOval(const Rect.fromLTWH(100, 30, testWidth, testHeight));
rc.clipPath(path);
final Path paintPath = Path();
paintPath.moveTo(testWidth / 2, 0);
paintPath.lineTo(testWidth, testHeight);
paintPath.lineTo(0, testHeight);
paintPath.close();
rc.drawPath(
paintPath,
engine.SurfacePaint()
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill,
);
rc.restore();
await canvasScreenshot(rc, 'triangle_clipped_by_oval');
});
// Regression test for https://github.com/flutter/flutter/issues/78782
test('Clips on Safari when clip bounds off screen', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.save();
const double testWidth = 200;
const double testHeight = 150;
final Path paintPath = Path();
paintPath.addRect(const Rect.fromLTWH(-50, 0, testWidth, testHeight));
paintPath.close();
rc.drawPath(
paintPath,
engine.SurfacePaint()
..color = const Color(0xFF000000)
..style = PaintingStyle.stroke,
);
final Path path = Path();
path.moveTo(-200, 0);
path.lineTo(100, 75);
path.lineTo(-200, 150);
path.close();
rc.clipPath(path);
rc.drawImageRect(
createTestImage(),
const Rect.fromLTRB(0, 0, testWidth, testHeight),
const Rect.fromLTWH(-50, 0, testWidth, testHeight),
engine.SurfacePaint(),
);
rc.restore();
await canvasScreenshot(
rc,
'image_clipped_by_triangle_off_screen',
region: const Rect.fromLTWH(0, 0, 600, 800),
);
});
// Tests oval clipping using border radius 50%.
test('Clips against oval', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.save();
const double testWidth = 200;
const double testHeight = 150;
final Path paintPath = Path();
paintPath.addRect(const Rect.fromLTWH(-50, 0, testWidth, testHeight));
paintPath.close();
rc.drawPath(
paintPath,
engine.SurfacePaint()
..color = const Color(0xFF000000)
..style = PaintingStyle.stroke,
);
final Path path = Path();
path.addOval(const Rect.fromLTRB(-200, 0, 100, 150));
rc.clipPath(path);
rc.drawImageRect(
createTestImage(),
const Rect.fromLTRB(0, 0, testWidth, testHeight),
const Rect.fromLTWH(-50, 0, testWidth, testHeight),
engine.SurfacePaint(),
);
rc.restore();
await canvasScreenshot(
rc,
'image_clipped_by_oval_path',
region: const Rect.fromLTWH(0, 0, 600, 800),
);
});
test('Clips with fillType evenOdd', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.save();
const double testWidth = 400;
const double testHeight = 350;
// draw RGB test image
rc.drawImageRect(
createTestImage(),
const Rect.fromLTRB(0, 0, testWidth, testHeight),
const Rect.fromLTWH(0, 0, testWidth, testHeight),
engine.SurfacePaint(),
);
// draw a clipping path with:
// 1) an outside larger rectangle
// 2) a smaller inner rectangle specified by a path
final Path path = Path();
path.addRect(const Rect.fromLTWH(0, 0, testWidth, testHeight));
const double left = 25;
const double top = 30;
const double right = 300;
const double bottom = 250;
path
..moveTo(left, top)
..lineTo(right, top)
..lineTo(right, bottom)
..lineTo(left, bottom)
..close();
path.fillType = PathFillType.evenOdd;
rc.clipPath(path);
// draw an orange paint path of size testWidth and testHeight
final Path paintPath = Path();
paintPath.addRect(const Rect.fromLTWH(0, 0, testWidth, testHeight));
paintPath.close();
rc.drawPath(
paintPath,
engine.SurfacePaint()
..color = const Color(0xFFFF9800)
..style = PaintingStyle.fill,
);
rc.restore();
// when fillType is set to evenOdd from the clipping path, expect the inner
// rectangle should clip some of the orange painted portion, revealing the RGB testImage
await canvasScreenshot(
rc,
'clipPath_uses_fillType_evenOdd',
region: const Rect.fromLTWH(0, 0, 600, 800),
);
});
}
engine.HtmlImage createTestImage({int width = 200, int height = 150}) {
final engine.DomCanvasElement canvas = engine.createDomCanvasElement(
width: width,
height: height,
);
final engine.DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.fillStyle = '#E04040';
ctx.fillRect(0, 0, width / 3, height);
ctx.fill();
ctx.fillStyle = '#40E080';
ctx.fillRect(width / 3, 0, width / 3, height);
ctx.fill();
ctx.fillStyle = '#2040E0';
ctx.fillRect(2 * width / 3, 0, width / 3, height);
ctx.fill();
final engine.DomHTMLImageElement imageElement = engine.createDomHTMLImageElement();
imageElement.src = js_util.callMethod<String>(canvas, 'toDataURL', <dynamic>[]);
return engine.HtmlImage(imageElement, width, height);
}

View File

@@ -1,79 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' as engine;
import 'package:ui/ui.dart' hide TextStyle;
import '../common/test_initialization.dart';
import 'screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
/// Tests context save/restore.
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
setUpUnitTests(setUpTestViewDimensions: false);
// Regression test for https://github.com/flutter/flutter/issues/49429
// Should clip with correct transform.
test('Clips image with oval clip path', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
final engine.SurfacePaint paint =
Paint() as engine.SurfacePaint
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill;
rc.save();
final Path ovalPath = Path();
ovalPath.addOval(const Rect.fromLTWH(100, 30, 200, 100));
rc.clipPath(ovalPath);
rc.translate(-500, -500);
rc.save();
rc.translate(500, 500);
rc.drawPath(ovalPath, paint);
// The line below was causing SaveClipStack to incorrectly set
// transform before path painting.
rc.translate(-1000, -1000);
rc.save();
rc.restore();
rc.restore();
rc.restore();
// The rectangle should paint without clipping since we restored
// context.
rc.drawRect(const Rect.fromLTWH(0, 0, 4, 200), paint);
await canvasScreenshot(rc, 'context_save_restore_transform', canvasRect: screenRect);
});
test('Should restore clip path', () async {
final engine.RecordingCanvas rc = engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
final Paint goodPaint =
Paint()
..color = const Color(0x8000FF00)
..style = PaintingStyle.fill;
final Paint badPaint =
Paint()
..color = const Color(0xFFFF0000)
..style = PaintingStyle.fill;
rc.save();
final Path ovalPath = Path();
ovalPath.addOval(const Rect.fromLTWH(100, 30, 200, 100));
rc.clipPath(ovalPath);
rc.translate(-500, -500);
rc.save();
rc.restore();
// The rectangle should be clipped against oval.
rc.drawRect(const Rect.fromLTWH(0, 0, 300, 300), badPaint as engine.SurfacePaint);
rc.restore();
// The rectangle should paint without clipping since we restored
// context.
rc.drawRect(const Rect.fromLTWH(0, 0, 200, 200), goodPaint as engine.SurfacePaint);
await canvasScreenshot(rc, 'context_save_restore_clip', canvasRect: screenRect);
});
}

View File

@@ -1,112 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import 'package:web_engine_tester/golden_tester.dart';
import '../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
final SurfacePaint testPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFFFF00FF);
setUpUnitTests();
// Regression test for https://github.com/flutter/flutter/issues/51514
test("Canvas is reused and z-index doesn't leak across paints", () async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect, RenderStrategy());
const Rect region = Rect.fromLTWH(0, 0, 500, 500);
// Draw first frame into engine canvas.
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400));
final Path path =
Path()
..moveTo(3, 0)
..lineTo(100, 97);
rc.drawPath(path, testPaint);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
engineCanvas.endOfPaint();
DomElement sceneElement = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
sceneElement.append(engineCanvas.rootElement);
domDocument.body!.append(sceneElement);
final DomCanvasElement canvas = domDocument.querySelector('canvas')! as DomCanvasElement;
// ! Since canvas is first element, it should have zIndex = -1 for correct
// paint order.
expect(canvas.style.zIndex, '-1');
// Add id to canvas element to test for reuse.
const String kTestId = 'test-id-5698';
canvas.id = kTestId;
sceneElement.remove();
// Clear so resources are marked for reuse.
engineCanvas.clear();
// Now paint a second scene to same [BitmapCanvas] but paint an image
// before the path to move canvas element into second position.
final RecordingCanvas rc2 = RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400));
final Path path2 =
Path()
..moveTo(3, 0)
..quadraticBezierTo(100, 0, 100, 100);
rc2.drawImage(_createRealTestImage(), Offset.zero, SurfacePaint());
rc2.drawPath(path2, testPaint);
rc2.endRecording();
rc2.apply(engineCanvas, screenRect);
sceneElement = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
sceneElement.append(engineCanvas.rootElement);
domDocument.body!.append(sceneElement);
final DomCanvasElement canvas2 = domDocument.querySelector('canvas')! as DomCanvasElement;
// ZIndex should have been cleared since we have image element preceding
// canvas.
expect(canvas.style.zIndex != '-1', isTrue);
expect(canvas2.id, kTestId);
await matchGoldenFile('bitmap_canvas_reuse_zindex.png', region: region);
});
}
const String _base64Encoded20x20TestImage =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA'
'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj'
'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg==';
HtmlImage _createRealTestImage() {
return HtmlImage(
createDomHTMLImageElement()..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage',
20,
20,
);
}

View File

@@ -1,87 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../common/mock_engine_canvas.dart';
import '../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
await bootstrapAndRunApp(withImplicitView: true);
group('EngineCanvas', () {
late MockEngineCanvas mockCanvas;
late ui.Paragraph paragraph;
void testCanvas(
String description,
void Function(EngineCanvas canvas) testFn, {
ui.Rect canvasSize = const ui.Rect.fromLTWH(0, 0, 100, 100),
ui.VoidCallback? whenDone,
}) {
test(description, () {
testFn(BitmapCanvas(canvasSize, RenderStrategy()));
testFn(DomCanvas(domDocument.createElement('flt-picture')));
testFn(mockCanvas = MockEngineCanvas());
whenDone?.call();
});
}
testCanvas(
'draws laid out paragraph',
(EngineCanvas canvas) {
const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, 100, 100);
final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect);
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle());
builder.addText('sample');
paragraph = builder.build();
paragraph.layout(const ui.ParagraphConstraints(width: 100));
recordingCanvas.drawParagraph(paragraph, const ui.Offset(10, 10));
recordingCanvas.endRecording();
canvas.clear();
recordingCanvas.apply(canvas, screenRect);
},
whenDone: () {
expect(mockCanvas.methodCallLog, hasLength(3));
MockCanvasCall call = mockCanvas.methodCallLog[0];
expect(call.methodName, 'clear');
call = mockCanvas.methodCallLog[1];
expect(call.methodName, 'drawParagraph');
final Map<dynamic, dynamic> arguments = call.arguments as Map<dynamic, dynamic>;
expect(arguments['paragraph'], paragraph);
expect(arguments['offset'], const ui.Offset(10, 10));
},
);
testCanvas(
'ignores paragraphs that were not laid out',
(EngineCanvas canvas) {
const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, 100, 100);
final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect);
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle());
builder.addText('sample');
final ui.Paragraph paragraph = builder.build();
recordingCanvas.drawParagraph(paragraph, const ui.Offset(10, 10));
recordingCanvas.endRecording();
canvas.clear();
recordingCanvas.apply(canvas, screenRect);
},
whenDone: () {
expect(mockCanvas.methodCallLog, hasLength(2));
expect(mockCanvas.methodCallLog[0].methodName, 'clear');
expect(mockCanvas.methodCallLog[1].methodName, 'endOfPaint');
},
);
});
}

View File

@@ -1,79 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 500, 500);
late BitmapCanvas canvas;
setUp(() {
canvas = BitmapCanvas(region, RenderStrategy());
});
tearDown(() {
canvas.rootElement.remove();
});
test('draws paths using nonzero and evenodd winding rules', () async {
paintPaths(canvas);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_path_winding.png', region: region);
});
}
void paintPaths(BitmapCanvas canvas) {
canvas.drawRect(
const Rect.fromLTRB(0, 0, 500, 500),
SurfacePaintData()
..color = 0xFFFFFFFF
..style = PaintingStyle.fill,
); // white
final SurfacePaint paintFill =
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xFF00B0FF);
final SurfacePaint paintStroke =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = const Color(0xFFE00000);
final Path path1 =
Path()
..fillType = PathFillType.evenOdd
..moveTo(50, 0)
..lineTo(21, 90)
..lineTo(98, 35)
..lineTo(2, 35)
..lineTo(79, 90)
..close()
..addRect(const Rect.fromLTWH(20, 100, 200, 50))
..addRect(const Rect.fromLTWH(40, 120, 160, 10));
final Path path2 =
Path()
..fillType = PathFillType.nonZero
..moveTo(50, 200)
..lineTo(21, 290)
..lineTo(98, 235)
..lineTo(2, 235)
..lineTo(79, 290)
..close()
..addRect(const Rect.fromLTWH(20, 300, 200, 50))
..addRect(const Rect.fromLTWH(40, 320, 160, 10));
canvas.drawPath(path1, paintFill.paintData);
canvas.drawPath(path2, paintFill.paintData);
canvas.drawPath(path1, paintStroke.paintData);
canvas.drawPath(path2, paintStroke.paintData);
}

View File

@@ -1,90 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../common/test_initialization.dart';
import 'paragraph/helper.dart';
import 'screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
/// Regression test for https://github.com/flutter/flutter/issues/64734.
test('Clips using difference', () async {
const Offset shift = Offset(8, 8);
const Rect region = Rect.fromLTRB(0, 0, 400, 300);
final RecordingCanvas canvas = RecordingCanvas(region);
final Rect titleRect = const Rect.fromLTWH(20, 0, 50, 20).shift(shift);
final SurfacePaint paint =
SurfacePaint()
..style = PaintingStyle.stroke
..color = const Color(0xff000000)
..strokeWidth = 1;
canvas.save();
try {
final Rect borderRect = Rect.fromLTRB(0, 10, region.width, region.height).shift(shift);
canvas.clipRect(titleRect, ClipOp.difference);
canvas.drawRect(borderRect, paint);
} finally {
canvas.restore();
}
canvas.drawRect(titleRect, paint);
await canvasScreenshot(
canvas,
'clip_op_difference',
region: const Rect.fromLTRB(0, 0, 420, 360),
);
});
/// Regression test for https://github.com/flutter/flutter/issues/86345
test('Clips with zero width or height', () async {
const Rect region = Rect.fromLTRB(0, 0, 400, 300);
final RecordingCanvas canvas = RecordingCanvas(region);
final SurfacePaint paint =
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xff00ff00);
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..color = const Color(0xffff0000)
..strokeWidth = 1;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
final double x = 10 + i * 70;
final double y = 10 + j * 70;
canvas.save();
// Clip.
canvas.clipRect(Rect.fromLTWH(x, y, i * 25, j * 25), ClipOp.intersect);
// Draw the blue (clipped) rect.
canvas.drawRect(Rect.fromLTWH(x, y, 50, 50), paint);
final Paragraph p = plain(
EngineParagraphStyle(fontFamily: 'Roboto', fontSize: 34),
'23',
textStyle: EngineTextStyle.only(color: const Color(0xff0000ff)),
);
p.layout(const ParagraphConstraints(width: double.infinity));
canvas.drawParagraph(p, Offset(x, y));
canvas.restore();
// Draw the red border.
canvas.drawRect(Rect.fromLTWH(x, y, 50, 50), borderPaint);
}
}
await canvasScreenshot(canvas, 'clip_zero_width_height', region: region);
});
}

View File

@@ -1,191 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide BackdropFilterEngineLayer, ClipRectEngineLayer;
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
import '../../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
setUp(() async {
debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
for (final DomNode scene in domDocument.querySelectorAll('flt-scene')) {
scene.remove();
}
});
// The black circle on the left should not be blurred since it is outside
// the clip boundary around backdrop filter. However there should be only
// one red dot since the other one should be blurred by filter.
test('Background should only blur at ancestor clip boundary', () async {
const Rect region = Rect.fromLTWH(0, 0, 190, 130);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground(region);
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushClipRect(const Rect.fromLTRB(10, 10, 180, 120));
final Picture circles1 = _drawTestPictureWithCircles(region, 30, 30);
builder.addPicture(Offset.zero, circles1);
builder.pushClipRect(const Rect.fromLTRB(60, 10, 180, 120));
builder.pushBackdropFilter(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0));
final Picture circles2 = _drawTestPictureWithCircles(region, 90, 30);
builder.addPicture(Offset.zero, circles2);
builder.pop();
builder.pop();
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('backdrop_filter_clip.png', region: region);
});
test('Background should only blur at ancestor clip boundary after move', () async {
const Rect region = Rect.fromLTWH(0, 0, 190, 130);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground(region);
builder.addPicture(Offset.zero, backgroundPicture);
final ClipRectEngineLayer clipEngineLayer = builder.pushClipRect(
const Rect.fromLTRB(10, 10, 180, 120),
);
final Picture circles1 = _drawTestPictureWithCircles(region, 30, 30);
builder.addPicture(Offset.zero, circles1);
final ClipRectEngineLayer clipEngineLayer2 = builder.pushClipRect(
const Rect.fromLTRB(60, 10, 180, 120),
);
final BackdropFilterEngineLayer oldBackdropFilterLayer = builder.pushBackdropFilter(
ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
);
final Picture circles2 = _drawTestPictureWithCircles(region, 90, 30);
builder.addPicture(Offset.zero, circles2);
builder.pop();
builder.pop();
builder.pop();
builder.build();
// Now reparent filter layer in next scene.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.addPicture(Offset.zero, backgroundPicture);
builder2.pushClipRect(const Rect.fromLTRB(10, 10, 180, 120), oldLayer: clipEngineLayer);
builder2.addPicture(Offset.zero, circles1);
builder2.pushClipRect(const Rect.fromLTRB(10, 75, 180, 120), oldLayer: clipEngineLayer2);
builder2.pushBackdropFilter(
ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
oldLayer: oldBackdropFilterLayer,
);
builder2.addPicture(Offset.zero, circles2);
builder2.pop();
builder2.pop();
builder2.pop();
domDocument.body!.append(builder2.build().webOnlyRootElement!);
await matchGoldenFile('backdrop_filter_clip_moved.png', region: region);
});
// The blur filter should be applied to the background inside the clip even
// though there are no children of the backdrop filter.
test('Background should blur even if child does not paint', () async {
const Rect region = Rect.fromLTWH(0, 0, 190, 130);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground(region);
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushClipRect(const Rect.fromLTRB(10, 10, 180, 120));
final Picture circles1 = _drawTestPictureWithCircles(region, 30, 30);
builder.addPicture(Offset.zero, circles1);
builder.pushClipRect(const Rect.fromLTRB(60, 10, 180, 120));
builder.pushBackdropFilter(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0));
builder.pop();
builder.pop();
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('backdrop_filter_no_child_rendering.png', region: region);
});
test('colorFilter as imageFilter', () async {
const Rect region = Rect.fromLTWH(0, 0, 190, 130);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground(region);
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushClipRect(const Rect.fromLTRB(10, 10, 180, 120));
final Picture circles1 = _drawTestPictureWithCircles(region, 30, 30);
// current background color is light green, apply a light yellow colorFilter
const ColorFilter colorFilter = ColorFilter.mode(Color(0xFFFFFFB1), BlendMode.modulate);
builder.pushBackdropFilter(colorFilter);
builder.addPicture(Offset.zero, circles1);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('backdrop_filter_colorFilter_as_imageFilter.png', region: region);
});
}
Picture _drawTestPictureWithCircles(Rect region, double offsetX, double offsetY) {
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(region);
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 10),
10,
SurfacePaint()..style = PaintingStyle.fill,
);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 10),
10,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(255, 0, 0, 1),
);
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 60),
10,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 255, 0, 1),
);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 60),
10,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 0, 255, 1),
);
return recorder.endRecording();
}
Picture _drawBackground(Rect region) {
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(region);
canvas.drawRect(
region.deflate(8.0),
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xFFE0FFE0),
);
return recorder.endRecording();
}

View File

@@ -1,121 +0,0 @@
// 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:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
test('Blend circles with difference and color', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.drawRect(
const Rect.fromLTRB(0, 0, 400, 400),
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromARGB(255, 255, 255, 255),
);
rc.drawCircle(
const Offset(100, 100),
80.0,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromARGB(128, 255, 0, 0)
..blendMode = BlendMode.difference,
);
rc.drawCircle(
const Offset(170, 100),
80.0,
SurfacePaint()
..style = PaintingStyle.fill
..blendMode = BlendMode.color
..color = const Color.fromARGB(128, 0, 255, 0),
);
rc.drawCircle(
const Offset(135, 170),
80.0,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromARGB(128, 255, 0, 0),
);
rc.restore();
await canvasScreenshot(rc, 'canvas_blend_circle_diff_color');
});
test('Blend circle and text with multiply', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.drawRect(
const Rect.fromLTRB(0, 0, 400, 400),
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromARGB(255, 255, 255, 255),
);
rc.drawCircle(
const Offset(100, 100),
80.0,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromARGB(128, 255, 0, 0)
..blendMode = BlendMode.difference,
);
rc.drawCircle(
const Offset(170, 100),
80.0,
SurfacePaint()
..style = PaintingStyle.fill
..blendMode = BlendMode.color
..color = const Color.fromARGB(128, 0, 255, 0),
);
rc.drawCircle(
const Offset(135, 170),
80.0,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color.fromARGB(128, 255, 0, 0),
);
rc.drawImage(
createTestImage(),
const Offset(135.0, 130.0),
SurfacePaint()..blendMode = BlendMode.multiply,
);
rc.restore();
await canvasScreenshot(rc, 'canvas_blend_image_multiply');
});
}
HtmlImage createTestImage() {
const int width = 100;
const int height = 50;
final DomCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D 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();
final DomHTMLImageElement imageElement = createDomHTMLImageElement();
imageElement.src = js_util.callMethod<String>(canvas, 'toDataURL', <dynamic>[]);
return HtmlImage(imageElement, width, height);
}

View File

@@ -1,174 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
import '../testimage.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
SurfacePaint makePaint() => Paint() as SurfacePaint;
Future<void> testMain() async {
setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false);
const Color red = Color(0xFFFF0000);
const Color green = Color(0xFF00FF00);
const Color blue = Color(0xFF2196F3);
const Color white = Color(0xFFFFFFFF);
const Color grey = Color(0xFF808080);
const Color black = Color(0xFF000000);
final List<List<BlendMode>> modes = <List<BlendMode>>[
<BlendMode>[
BlendMode.clear,
BlendMode.src,
BlendMode.dst,
BlendMode.srcOver,
BlendMode.dstOver,
BlendMode.srcIn,
BlendMode.dstIn,
BlendMode.srcOut,
],
<BlendMode>[
BlendMode.dstOut,
BlendMode.srcATop,
BlendMode.dstATop,
BlendMode.xor,
BlendMode.plus,
BlendMode.modulate,
BlendMode.screen,
BlendMode.overlay,
],
<BlendMode>[
BlendMode.darken,
BlendMode.lighten,
BlendMode.colorDodge,
BlendMode.hardLight,
BlendMode.softLight,
BlendMode.difference,
BlendMode.exclusion,
BlendMode.multiply,
],
<BlendMode>[BlendMode.hue, BlendMode.saturation, BlendMode.color, BlendMode.luminosity],
];
for (int blendGroup = 0; blendGroup < 4; ++blendGroup) {
test('Draw image with Group$blendGroup blend modes', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 400));
rc.save();
final List<BlendMode> blendModes = modes[blendGroup];
for (int row = 0; row < blendModes.length; row++) {
// draw white background for first 4, black for next 4 blends.
final double top = row * 50.0;
rc.drawRect(Rect.fromLTWH(0, top, 200, 50), makePaint()..color = white);
rc.drawRect(Rect.fromLTWH(200, top, 200, 50), makePaint()..color = grey);
final BlendMode blendMode = blendModes[row];
rc.drawImage(
createFlutterLogoTestImage(),
Offset(0, top),
makePaint()..colorFilter = EngineColorFilter.mode(red, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(50, top),
makePaint()..colorFilter = EngineColorFilter.mode(green, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(100, top),
makePaint()..colorFilter = EngineColorFilter.mode(blue, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(150, top),
makePaint()..colorFilter = EngineColorFilter.mode(black, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(200, top),
makePaint()..colorFilter = EngineColorFilter.mode(red, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(250, top),
makePaint()..colorFilter = EngineColorFilter.mode(green, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(300, top),
makePaint()..colorFilter = EngineColorFilter.mode(blue, blendMode),
);
rc.drawImage(
createFlutterLogoTestImage(),
Offset(350, top),
makePaint()..colorFilter = EngineColorFilter.mode(black, blendMode),
);
}
rc.restore();
await canvasScreenshot(rc, 'canvas_image_blend_group$blendGroup');
}, skip: isSafari);
}
// Regression test for https://github.com/flutter/flutter/issues/56971
test('Draws image and paragraph at same vertical position', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 400));
rc.save();
rc.drawRect(const Rect.fromLTWH(0, 50, 200, 50), makePaint()..color = white);
rc.drawImage(
createFlutterLogoTestImage(),
const Offset(0, 50),
makePaint()..colorFilter = const EngineColorFilter.mode(red, BlendMode.srcIn),
);
final Paragraph paragraph = createTestParagraph();
const double textLeft = 80.0;
const double textTop = 50.0;
const double widthConstraint = 300.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
rc.restore();
await canvasScreenshot(rc, 'canvas_image_blend_and_text');
});
test('Does not re-use styles with same image src', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 400));
final HtmlImage flutterImage = createFlutterLogoTestImage();
rc.save();
rc.drawRect(const Rect.fromLTWH(0, 50, 200, 50), makePaint()..color = white);
rc.drawImage(
flutterImage,
const Offset(0, 50),
makePaint()..colorFilter = const EngineColorFilter.mode(red, BlendMode.modulate),
);
// Expect that the colorFilter is only applied to the first image, since the
// colorFilter is applied to a clone of the flutterImage and not the original
rc.drawImage(flutterImage, const Offset(0, 100), makePaint());
rc.restore();
await canvasScreenshot(rc, 'canvas_image_same_src');
});
}
Paragraph createTestParagraph() {
final ParagraphBuilder builder = ParagraphBuilder(
ParagraphStyle(
fontFamily: 'Ahem',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 14.0,
),
);
builder.addText('FOO');
return builder.build();
}

View File

@@ -1,50 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../screenshot.dart';
import '../testimage.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
SurfacePaint makePaint() => Paint() as SurfacePaint;
Future<void> testMain() async {
const double screenWidth = 100.0;
const double screenHeight = 100.0;
const Rect region = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
// Regression test for https://github.com/flutter/flutter/issues/76966
test('Draws image with dstATop color filter', () async {
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawImage(
createFlutterLogoTestImage(),
const Offset(10, 10),
makePaint()..colorFilter = const EngineColorFilter.mode(Color(0x40000000), BlendMode.dstATop),
);
await canvasScreenshot(canvas, 'image_color_fiter_dstatop', region: region);
});
test('Draws image with matrix color filter', () async {
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawImage(
createFlutterLogoTestImage(),
const Offset(10, 10),
makePaint()
..colorFilter = const EngineColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0, //
0, 0, 0, 1, 0, //
]),
);
await canvasScreenshot(canvas, 'image_matrix_color_fiter', region: region);
});
}

View File

@@ -1,160 +0,0 @@
// 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 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
tearDown(() {
ContextStateHandle.debugEmulateWebKitMaskFilter = false;
});
// Regression test for https://github.com/flutter/flutter/issues/55930
void testMaskFilterBlur({bool isWebkit = false}) {
final String browser = isWebkit ? 'Safari' : 'Chrome';
test('renders MaskFilter.blur in $browser', () async {
const double screenWidth = 800.0;
const double screenHeight = 150.0;
const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, screenWidth, screenHeight);
ContextStateHandle.debugEmulateWebKitMaskFilter = isWebkit;
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.translate(0, 75);
final SurfacePaint paint =
SurfacePaint()..maskFilter = const ui.MaskFilter.blur(ui.BlurStyle.normal, 5);
rc.translate(50, 0);
rc.drawRect(ui.Rect.fromCircle(center: ui.Offset.zero, radius: 30), paint);
rc.translate(100, 0);
paint.color = const ui.Color(0xFF00FF00);
rc.drawRRect(
ui.RRect.fromRectAndRadius(
ui.Rect.fromCircle(center: ui.Offset.zero, radius: 30),
const ui.Radius.circular(20),
),
paint,
);
rc.translate(100, 0);
paint.color = const ui.Color(0xFF0000FF);
rc.drawCircle(ui.Offset.zero, 30, paint);
rc.translate(100, 0);
paint.color = const ui.Color(0xFF00FFFF);
rc.drawPath(
SurfacePath()
..moveTo(-20, 0)
..lineTo(0, -50)
..lineTo(20, 0)
..lineTo(0, 50)
..close(),
paint,
);
rc.translate(100, 0);
paint.color = const ui.Color(0xFFFF00FF);
rc.drawOval(ui.Rect.fromCenter(center: ui.Offset.zero, width: 40, height: 100), paint);
rc.translate(100, 0);
paint.color = const ui.Color(0xFF888800);
paint.strokeWidth = 5;
rc.drawLine(const ui.Offset(-20, -50), const ui.Offset(20, 50), paint);
rc.translate(100, 0);
paint.color = const ui.Color(0xFF888888);
rc.drawDRRect(
ui.RRect.fromRectAndRadius(
ui.Rect.fromCircle(center: ui.Offset.zero, radius: 35),
const ui.Radius.circular(20),
),
ui.RRect.fromRectAndRadius(
ui.Rect.fromCircle(center: ui.Offset.zero, radius: 15),
const ui.Radius.circular(7),
),
paint,
);
rc.translate(100, 0);
paint.color = const ui.Color(0xFF6500C9);
rc.drawRawPoints(
ui.PointMode.points,
Float32List.fromList(<double>[-10, -10, -10, 10, 10, -10, 10, 10]),
paint,
);
await canvasScreenshot(rc, 'mask_filter_$browser', region: screenRect);
});
test('renders transformed MaskFilter.blur in $browser', () async {
const double screenWidth = 300.0;
const double screenHeight = 300.0;
const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, screenWidth, screenHeight);
ContextStateHandle.debugEmulateWebKitMaskFilter = isWebkit;
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.translate(150, 150);
final SurfacePaint paint =
SurfacePaint()..maskFilter = const ui.MaskFilter.blur(ui.BlurStyle.normal, 5);
const List<ui.Color> colors = <ui.Color>[
ui.Color(0xFF000000),
ui.Color(0xFF00FF00),
ui.Color(0xFF0000FF),
ui.Color(0xFF00FFFF),
ui.Color(0xFFFF00FF),
ui.Color(0xFF888800),
ui.Color(0xFF888888),
ui.Color(0xFF6500C9),
];
for (final ui.Color color in colors) {
paint.color = color;
rc.rotate(math.pi / 4);
rc.drawRect(ui.Rect.fromCircle(center: const ui.Offset(90, 0), radius: 20), paint);
}
await canvasScreenshot(rc, 'mask_filter_transformed_$browser', region: screenRect);
});
}
testMaskFilterBlur();
testMaskFilterBlur(isWebkit: true);
for (final int testDpr in <int>[1, 2, 4]) {
test('MaskFilter.blur blurs correctly for device-pixel ratio $testDpr', () async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(testDpr.toDouble());
const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, 150, 150);
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.translate(0, 75);
final SurfacePaint paint =
SurfacePaint()..maskFilter = const ui.MaskFilter.blur(ui.BlurStyle.normal, 5);
rc.translate(75, 0);
rc.drawRect(ui.Rect.fromCircle(center: ui.Offset.zero, radius: 30), paint);
await canvasScreenshot(rc, 'mask_filter_blur_dpr_$testDpr', region: screenRect);
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(1.0);
});
}
}

View File

@@ -1,128 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
import '../../common/test_initialization.dart';
const Rect region = Rect.fromLTWH(0, 0, 500, 500);
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
setUp(() async {
debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
for (final DomNode scene in domDocument.querySelectorAll('flt-scene')) {
scene.remove();
}
});
test('Should apply color filter to image', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground();
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushColorFilter(const EngineColorFilter.mode(Color(0xF0000080), BlendMode.color));
final Picture circles1 = _drawTestPictureWithCircles(30, 30);
builder.addPicture(Offset.zero, circles1);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
// TODO(ferhat): update golden for this test after canvas sandwich detection is
// added to RecordingCanvas.
await matchGoldenFile('color_filter_blendMode_color.png', region: region);
});
test('Should apply matrix color filter to image', () async {
final List<double> colorMatrix = <double>[
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0, //
0, 0, 0, 1, 0, //
];
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground();
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushColorFilter(EngineColorFilter.matrix(colorMatrix));
final Picture circles1 = _drawTestPictureWithCircles(30, 30);
builder.addPicture(Offset.zero, circles1);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('color_filter_matrix.png', region: region);
});
/// Regression test for https://github.com/flutter/flutter/issues/85733
test('Should apply mode color filter to circles', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground();
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushColorFilter(const ColorFilter.mode(Color(0xFFFF0000), BlendMode.srcIn));
final Picture circles1 = _drawTestPictureWithCircles(30, 30);
builder.addPicture(Offset.zero, circles1);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('color_filter_mode.png', region: region);
});
}
Picture _drawTestPictureWithCircles(double offsetX, double offsetY) {
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 10),
10,
(Paint()..style = PaintingStyle.fill) as SurfacePaint,
);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 10),
10,
(Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(255, 0, 0, 1))
as SurfacePaint,
);
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 60),
10,
(Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 255, 0, 1))
as SurfacePaint,
);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 60),
10,
(Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 0, 255, 1))
as SurfacePaint,
);
return recorder.endRecording();
}
Picture _drawBackground() {
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
canvas.drawRect(
const Rect.fromLTWH(8, 8, 400.0 - 16, 400.0 - 16),
(Paint()
..style = PaintingStyle.fill
..color = const Color(0xFFE0FFE0))
as SurfacePaint,
);
return recorder.endRecording();
}

View File

@@ -1,683 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../../common/matchers.dart';
import '../../common/test_initialization.dart';
const ui.Rect region = ui.Rect.fromLTWH(0, 0, 500, 100);
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
setUp(() async {
// To debug test failures uncomment the following to visualize clipping
// layers:
// debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
for (final DomNode scene in domDocument.querySelectorAll('flt-scene')) {
scene.remove();
}
});
test('pushClipRect', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(const ui.Rect.fromLTRB(10, 10, 60, 60));
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_shifted_clip_rect.png', region: region);
});
test('pushClipRect with offset and transform', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 60);
builder.pushTransform(Matrix4.diagonal3Values(1, -1, 1).toFloat64());
builder.pushClipRect(const ui.Rect.fromLTRB(10, 10, 60, 60));
_drawTestPicture(builder);
builder.pop();
builder.pop();
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_clip_rect_with_offset_and_transform.png', region: region);
});
test('pushClipRect with offset and transform ClipOp none should not clip', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 80);
builder.pushTransform(Matrix4.diagonal3Values(1, -1, 1).toFloat64());
builder.pushClipRect(const ui.Rect.fromLTRB(10, 10, 60, 60), clipBehavior: ui.Clip.none);
_drawTestPicture(builder);
builder.pop();
builder.pop();
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_clip_rect_clipop_none.png', region: region);
});
test('pushClipRRect with offset and transform ClipOp none should not clip', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 80);
builder.pushTransform(Matrix4.diagonal3Values(1, -1, 1).toFloat64());
builder.pushClipRRect(
ui.RRect.fromRectAndRadius(
const ui.Rect.fromLTRB(10, 10, 60, 60),
const ui.Radius.circular(1),
),
clipBehavior: ui.Clip.none,
);
_drawTestPicture(builder);
builder.pop();
builder.pop();
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_clip_rrect_clipop_none.png', region: region);
});
test('pushClipRRect', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRRect(ui.RRect.fromLTRBR(10, 10, 60, 60, const ui.Radius.circular(5)));
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_shifted_clip_rrect.png', region: region);
});
test('pushImageFilter blur', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushImageFilter(ui.ImageFilter.blur(sigmaX: 1, sigmaY: 3));
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_image_filter.png', region: region);
});
test('pushImageFilter matrix', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushImageFilter(
ui.ImageFilter.matrix(
(Matrix4.identity()
..translate(40, 10)
..rotateZ(math.pi / 6)
..scale(0.75, 0.75))
.toFloat64(),
),
);
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_image_filter_matrix.png', region: region);
});
test('pushImageFilter using mode ColorFilter', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
// Applying the colorFilter should turn all the circles red.
builder.pushImageFilter(const ui.ColorFilter.mode(ui.Color(0xFFFF0000), ui.BlendMode.srcIn));
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_image_filter_using_mode_color_filter.png', region: region);
});
test('pushImageFilter using matrix ColorFilter', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
// Apply a "greyscale" color filter.
final List<double> colorMatrix = <double>[
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0, //
0, 0, 0, 1, 0, //
];
builder.pushImageFilter(ui.ColorFilter.matrix(colorMatrix));
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_image_filter_using_matrix_color_filter.png', region: region);
});
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 ui.SceneBuilder builder = ui.SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(ui.Offset.zero, 10000, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(0, 0, 500, 100));
// Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500
// https://github.com/flutter/flutter/issues/40395
}, skip: true);
// 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 ui.SceneBuilder builder = ui.SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.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 ui.SceneBuilder builder = ui.SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(
const ui.Offset(-100, -100),
20,
SurfacePaint()..style = ui.PaintingStyle.fill,
);
});
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, ui.Rect.zero);
expect(picture.debugExactGlobalCullRect, ui.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 ui.SceneBuilder builder = ui.SceneBuilder();
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(const ui.Offset(50, 50), 10, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.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 ui.SceneBuilder builder = ui.SceneBuilder();
builder.pushOffset(10, 10);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(const ui.Offset(50, 50), 10, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.pop();
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.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 ui.SceneBuilder builder = ui.SceneBuilder();
builder.pushOffset(0, 90);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.pop();
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.debugExactGlobalCullRect, const ui.Rect.fromLTRB(0, 70, 20, 100));
expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(0, -20, 20, 10));
// Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500
// https://github.com/flutter/flutter/issues/40395
}, skip: true);
// 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 SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(const ui.Rect.fromLTWH(10, 10, 60, 60));
builder.pushClipRect(const ui.Rect.fromLTWH(40, 40, 60, 60));
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(ui.Offset.zero, 10000, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_cull_rect_fills_layer_clip.png', region: region);
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(40, 40, 70, 70));
});
// 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 SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(const ui.Rect.fromLTWH(10, 10, 60, 60));
builder.pushClipRect(const ui.Rect.fromLTWH(40, 40, 60, 60));
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(const ui.Offset(80, 55), 30, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile(
'compositing_cull_rect_intersects_clip_and_paint_bounds.png',
region: region,
);
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(50, 40, 70, 70));
});
// 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 SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(const ui.Rect.fromLTWH(10, 10, 60, 60));
builder.pushClipRect(const ui.Rect.fromLTWH(40, 40, 60, 60));
builder.pushOffset(55, 70);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.pop(); // pushOffset
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_cull_rect_offset_inside_layer_clip.png', region: region);
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(-15.0, -20.0, 15.0, 0.0));
});
// 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 ui.SceneBuilder builder = ui.SceneBuilder();
builder.pushClipRect(const ui.Rect.fromLTWH(10, 10, 60, 60));
builder.pushClipRect(const ui.Rect.fromLTWH(40, 40, 60, 60));
builder.pushOffset(100, 50);
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill);
});
builder.pop(); // pushOffset
builder.pop(); // pushClipRect
builder.pop(); // pushClipRect
builder.build();
final PersistedPicture picture = enumeratePictures().single;
expect(picture.optimalLocalCullRect, ui.Rect.zero);
expect(picture.debugExactGlobalCullRect, ui.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 SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(80, 50);
builder.pushTransform(Matrix4.rotationZ(-math.pi / 4).toFloat64());
builder.pushClipRect(const ui.Rect.fromLTRB(-10, -10, 10, 10));
builder.pushTransform(Matrix4.rotationZ(math.pi / 4).toFloat64());
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawPaint(
SurfacePaint()
..color = const ui.Color.fromRGBO(0, 0, 255, 0.6)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(-5, -5, 5, 5),
SurfacePaint()
..color = const ui.Color.fromRGBO(0, 255, 0, 1.0)
..style = ui.PaintingStyle.fill,
);
});
builder.pop(); // pushTransform
builder.pop(); // pushClipRect
builder.pop(); // pushTransform
builder.pop(); // pushOffset
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_cull_rect_rotated.png', region: region);
final PersistedPicture picture = enumeratePictures().single;
expect(
picture.optimalLocalCullRect,
within(distance: 0.05, from: const ui.Rect.fromLTRB(-14.1, -14.1, 14.1, 14.1)),
);
});
test('pushClipPath', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Path path = ui.Path();
path.addRect(const ui.Rect.fromLTRB(10, 10, 60, 60));
builder.pushClipPath(path);
_drawTestPicture(builder);
builder.pop();
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_clip_path.png', region: region);
});
// 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 SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushTransform(
Matrix4.diagonal3Values(
EngineFlutterDisplay.instance.browserDevicePixelRatio,
EngineFlutterDisplay.instance.browserDevicePixelRatio,
1.0,
).toFloat64(),
);
// TODO(yjbanov): see the TODO below.
// final double screenWidth = domWindow.innerWidth.toDouble();
// final double screenHeight = domWindow.innerHeight.toDouble();
final Matrix4 scaleTransform = Matrix4.identity().scaled(0.5, 0.2);
builder.pushTransform(scaleTransform.toFloat64());
builder.pushOffset(400, 200);
builder.pushClipRect(const ui.Rect.fromLTRB(-200, -200, 200, 200));
builder.pushTransform(Matrix4.rotationY(45.0 * math.pi / 180.0).toFloat64());
builder.pushClipRect(const ui.Rect.fromLTRB(-140, -140, 140, 140));
builder.pushTransform(Matrix4.translationValues(0, 0, -50).toFloat64());
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
canvas.drawPaint(
SurfacePaint()
..color = const ui.Color.fromRGBO(0, 0, 255, 0.6)
..style = ui.PaintingStyle.fill,
);
// ui.Rect will be clipped.
canvas.drawRect(
const ui.Rect.fromLTRB(-150, -150, 150, 150),
SurfacePaint()
..color = const ui.Color.fromRGBO(0, 255, 0, 1.0)
..style = ui.PaintingStyle.fill,
);
// Should be outside the clip range.
canvas.drawRect(
const ui.Rect.fromLTRB(-150, -150, -140, -140),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 255, 0, 0)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(140, -150, 150, -140),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 255, 0, 0)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(-150, 140, -140, 150),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 255, 0, 0)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(140, 140, 150, 150),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 255, 0, 0)
..style = ui.PaintingStyle.fill,
);
// Should be inside clip range
canvas.drawRect(
const ui.Rect.fromLTRB(-100, -100, -90, -90),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(90, -100, 100, -90),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(-100, 90, -90, 100),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80)
..style = ui.PaintingStyle.fill,
);
canvas.drawRect(
const ui.Rect.fromLTRB(90, 90, 100, 100),
SurfacePaint()
..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80)
..style = ui.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
builder.pop(); // pushTransform scale devicepixelratio
domDocument.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('compositing_3d_rotate1.png', region: region);
// ignore: unused_local_variable
final PersistedPicture picture = enumeratePictures().single;
// TODO(yjbanov): 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: ui.Rect.fromLTRB(
// -140, -140, screenWidth - 360.0, screenHeight + 40.0)),
// );
});
// This test reproduces text blurriness when two pieces of text appear inside
// two nested clips:
//
// ┌───────────────────────┐
// │ text in outer clip │
// │ ┌────────────────────┐│
// │ │ text in inner clip ││
// │ └────────────────────┘│
// └───────────────────────┘
//
// This test clips using layers. See a similar test in `bitmap_canvas_golden_test.dart`,
// which clips using canvas.
//
// More details: https://github.com/flutter/flutter/issues/32274
test('renders clipped text with high quality', () async {
// To reproduce blurriness we need real clipping.
final CanvasParagraph paragraph =
(ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: 'Roboto'))
// Use a decoration to force rendering in DOM mode.
..pushStyle(
ui.TextStyle(
decoration: ui.TextDecoration.lineThrough,
decorationColor: const ui.Color(0x00000000),
),
)
..addText('Am I blurry?'))
.build()
as CanvasParagraph;
paragraph.layout(const ui.ParagraphConstraints(width: 1000));
final ui.Rect canvasSize = ui.Rect.fromLTRB(
0,
0,
paragraph.maxIntrinsicWidth + 16,
2 * paragraph.height + 32,
);
final ui.Rect outerClip = ui.Rect.fromLTRB(0.5, 0.5, canvasSize.right, canvasSize.bottom);
final ui.Rect innerClip = ui.Rect.fromLTRB(
0.5,
canvasSize.bottom / 2 + 0.5,
canvasSize.right,
canvasSize.bottom,
);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(outerClip);
{
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(outerClip);
canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5));
final ui.Picture picture = recorder.endRecording();
expect(paragraph.canDrawOnCanvas, isFalse);
builder.addPicture(ui.Offset.zero, picture);
}
builder.pushClipRect(innerClip);
{
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(innerClip);
canvas.drawParagraph(paragraph, ui.Offset(8.5, 8.5 + innerClip.top));
final ui.Picture picture = recorder.endRecording();
expect(paragraph.canDrawOnCanvas, isFalse);
builder.addPicture(ui.Offset.zero, picture);
}
builder.pop(); // inner clip
builder.pop(); // outer clip
final DomElement sceneElement = builder.build().webOnlyRootElement!;
expect(
sceneElement
.querySelectorAll('flt-paragraph')
.map<String>((DomElement e) => e.innerText)
.toList(),
<String>['Am I blurry?', 'Am I blurry?'],
reason: 'Expected to render text using HTML',
);
domDocument.body!.append(sceneElement);
await matchGoldenFile('compositing_draw_high_quality_text.png', region: canvasSize);
}, testOn: 'chrome');
}
void _drawTestPicture(ui.SceneBuilder builder) {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 100, 100));
canvas.drawCircle(const ui.Offset(10, 10), 10, SurfacePaint()..style = ui.PaintingStyle.fill);
canvas.drawCircle(
const ui.Offset(60, 10),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(255, 0, 0, 1),
);
canvas.drawCircle(
const ui.Offset(10, 60),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(0, 255, 0, 1),
);
canvas.drawCircle(
const ui.Offset(60, 60),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(0, 0, 255, 1),
);
final ui.Picture picture = recorder.endRecording();
builder.addPicture(ui.Offset.zero, picture);
}
typedef PaintCallback = void Function(RecordingCanvas canvas);
void drawWithBitmapCanvas(
ui.SceneBuilder builder,
PaintCallback callback, {
ui.Rect bounds = ui.Rect.largest,
}) {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(bounds);
canvas.debugEnforceArbitraryPaint();
callback(canvas);
final ui.Picture picture = recorder.endRecording();
builder.addPicture(ui.Offset.zero, picture);
}

View File

@@ -1,30 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
test('Should blur rectangles based on sigma.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
for (int blurSigma = 1; blurSigma < 10; blurSigma += 2) {
final SurfacePaint paint =
SurfacePaint()
..color = const Color(0xFF2fdfd2)
..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma.toDouble());
rc.drawRect(Rect.fromLTWH(15.0, 15.0 + blurSigma * 40, 200, 20), paint);
}
await canvasScreenshot(rc, 'dom_mask_filter_blur');
});
}

View File

@@ -1,37 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
final List<String> warnings = <String>[];
late void Function(String) oldPrintWarning;
setUpAll(() async {
oldPrintWarning = printWarning;
printWarning = (String warning) {
warnings.add(warning);
};
});
tearDownAll(() {
printWarning = oldPrintWarning;
});
test('Emit a warning when the HTML Renderer was picked.', () {
final Renderer chosenRenderer = renderer;
expect(chosenRenderer, isA<HtmlRenderer>());
expect(
warnings,
contains(contains('See: https://docs.flutter.dev/to/web-html-renderer-deprecation')),
);
});
}

View File

@@ -1,103 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
group('$adjustRectForDom', () {
test('does not change rect when not necessary', () async {
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
expect(adjustRectForDom(rect, SurfacePaintData()..style = PaintingStyle.fill), rect);
expect(
adjustRectForDom(
rect,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 0,
),
rect,
);
});
test('takes stroke width into consideration', () async {
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
expect(
adjustRectForDom(
rect,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 1,
),
const Rect.fromLTWH(9.5, 19.5, 139, 159),
);
expect(
adjustRectForDom(
rect,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 10,
),
const Rect.fromLTWH(5, 15, 130, 150),
);
expect(
adjustRectForDom(
rect,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 15,
),
const Rect.fromLTWH(2.5, 12.5, 125, 145),
);
});
test('flips rect when necessary', () {
Rect rect = const Rect.fromLTWH(100, 200, -40, -60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style = PaintingStyle.fill),
const Rect.fromLTWH(60, 140, 40, 60),
);
rect = const Rect.fromLTWH(100, 200, 40, -60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style = PaintingStyle.fill),
const Rect.fromLTWH(100, 140, 40, 60),
);
rect = const Rect.fromLTWH(100, 200, -40, 60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style = PaintingStyle.fill),
const Rect.fromLTWH(60, 200, 40, 60),
);
});
test('handles stroke width greater than width or height', () {
const Rect rect = Rect.fromLTWH(100, 200, 20, 70);
expect(
adjustRectForDom(
rect,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 50,
),
const Rect.fromLTWH(75, 175, 0, 20),
);
expect(
adjustRectForDom(
rect,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 80,
),
const Rect.fromLTWH(60, 160, 0, 0),
);
});
});
}

View File

@@ -1,132 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 400, 600);
late BitmapCanvas canvas;
setUp(() {
canvas = BitmapCanvas(region, RenderStrategy());
});
tearDown(() {
canvas.rootElement.remove();
});
test('draws arcs with largeArc , anticlockwise variations', () async {
paintArc(canvas, Offset.zero, distance: 20);
paintArc(canvas, const Offset(200, 0), largeArc: true, distance: 20);
paintArc(canvas, const Offset(0, 150), clockwise: true, distance: 20);
paintArc(canvas, const Offset(200, 150), largeArc: true, clockwise: true, distance: 20);
paintArc(canvas, const Offset(0, 300), distance: -20);
paintArc(canvas, const Offset(200, 300), largeArc: true, distance: -20);
paintArc(canvas, const Offset(0, 400), clockwise: true, distance: -20);
paintArc(canvas, const Offset(200, 400), largeArc: true, clockwise: true, distance: -20);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_arc_to_point.png', region: region);
});
test('Path.addArc that starts new path has correct start point', () async {
const Rect rect = Rect.fromLTWH(20, 20, 200, 200);
final Path p =
Path()
..fillType = PathFillType.evenOdd
..addRect(rect)
..addArc(
Rect.fromCircle(center: rect.center, radius: rect.size.shortestSide / 2),
0.25 * math.pi,
1.5 * math.pi,
);
canvas.drawPath(
p,
SurfacePaintData()
..color =
0xFFFF9800 // orange
..style = PaintingStyle.fill,
);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_addarc.png', region: region);
});
test('Should render counter clockwise arcs', () async {
final Path path = Path();
path.moveTo(149.999999999999997, 50);
path.lineTo(149.999999999999997, 20);
path.arcTo(
const Rect.fromLTRB(20, 20, 280, 280),
4.71238898038469,
5.759586531581287 - 4.71238898038469,
true,
);
path.lineTo(236.60254037844385, 99.99999999999999);
path.arcTo(
const Rect.fromLTRB(50, 50, 250, 250),
5.759586531581287,
4.71238898038469 - 5.759586531581287,
true,
);
path.lineTo(149.999999999999997, 20);
canvas.drawPath(
path,
SurfacePaintData()
..color =
0xFFFF9800 // orange
..style = PaintingStyle.fill,
);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_addarc_ccw.png', region: region);
});
}
void paintArc(
BitmapCanvas canvas,
Offset offset, {
bool largeArc = false,
bool clockwise = false,
double distance = 0,
}) {
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);
canvas.drawRect(
Rect.fromLTRB(startP.dx, startP.dy, endP.dx, endP.dy),
SurfacePaintData()
..strokeWidth = 1
..color =
0xFFFF9800 // orange
..style = PaintingStyle.stroke,
);
final Path path = Path();
path.moveTo(startP.dx, startP.dy);
path.arcToPoint(
endP,
rotation: 45,
radius: const Radius.elliptical(40, 60),
largeArc: largeArc,
clockwise: clockwise,
);
canvas.drawPath(
path,
SurfacePaintData()
..strokeWidth = 2
..color =
0x61000000 // black38
..style = PaintingStyle.stroke,
);
}

View File

@@ -1,83 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(emulateTesterEnvironment: false, setUpTestViewDimensions: false);
setUp(() async {
debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
});
tearDown(() {
for (final DomNode scene in domDocument.querySelectorAll('flt-scene')) {
scene.remove();
}
});
test('drawColor should cover entire viewport', () async {
const Rect region = Rect.fromLTWH(0, 0, 400, 400);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture testPicture = _drawTestPicture(region, useColor: true);
builder.addPicture(Offset.zero, testPicture);
await sceneScreenshot(builder, 'canvas_draw_color', region: region);
}, skip: true); // TODO(ferhat): matchGolden fails when a div covers viewport.
test('drawPaint should cover entire viewport', () async {
const Rect region = Rect.fromLTWH(0, 0, 400, 400);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture testPicture = _drawTestPicture(region);
builder.addPicture(Offset.zero, testPicture);
await sceneScreenshot(builder, 'canvas_draw_paint', region: region);
}, skip: true); // TODO(ferhat): matchGolden fails when a div covers viewport.);
}
Picture _drawTestPicture(Rect region, {bool useColor = false}) {
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
const Rect r = Rect.fromLTWH(0, 0, 200, 200);
final RecordingCanvas canvas = recorder.beginRecording(r);
canvas.drawRect(
region.deflate(8.0),
Paint() as SurfacePaint
..style = PaintingStyle.fill
..color = const Color(0xFFE0E0E0),
);
canvas.transform(Matrix4.translationValues(50, 50, 0).storage);
if (useColor) {
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1), BlendMode.srcOver);
} else {
canvas.drawPaint(
Paint() as SurfacePaint
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 0, 255, 1),
);
}
canvas.drawCircle(
Offset(r.width / 2, r.height / 2),
r.width / 2,
Paint() as SurfacePaint
..style = PaintingStyle.fill
..color = const Color.fromRGBO(255, 0, 0, 1),
);
return recorder.endRecording();
}

View File

@@ -1,836 +0,0 @@
// 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:convert';
import 'dart:js_util' as js_util;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import 'package:web_engine_tester/golden_tester.dart';
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false);
test('Paints image', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
rc.drawImage(createTestImage(), Offset.zero, SurfacePaint());
rc.restore();
await canvasScreenshot(rc, 'draw_image');
});
test(
'Images from raw data are composited when picture is roundtripped through toImage',
() async {
final Uint8List imageData = base64Decode(base64PngData);
final Codec codec = await instantiateImageCodec(imageData);
final FrameInfo frameInfo = await codec.getNextFrame();
codec.dispose();
const Rect bounds = Rect.fromLTRB(0, 0, 400, 300);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas scratchCanvas = recorder.beginRecording(bounds);
scratchCanvas.save();
scratchCanvas.drawImage(frameInfo.image, Offset.zero, SurfacePaint());
scratchCanvas.restore();
final Picture picture = recorder.endRecording();
final Image image = await picture.toImage(400, 300);
final RecordingCanvas rc = RecordingCanvas(bounds);
rc.save();
rc.drawImage(image, Offset.zero, SurfacePaint());
rc.restore();
await canvasScreenshot(rc, 'draw_raw_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.zero, SurfacePaint());
rc.restore();
await canvasScreenshot(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(), const Offset(30, 20), SurfacePaint());
rc.restore();
await canvasScreenshot(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);
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.drawImageRect(
testImage,
Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.drawImageRect(
testImage,
Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.restore();
await canvasScreenshot(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));
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.save();
rc.clipRRect(
RRect.fromLTRBR(100, 30, 2 * testWidth, 2 * testHeight, const Radius.circular(16)),
);
rc.drawImageRect(
testImage,
Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.restore();
await canvasScreenshot(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);
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.drawImageRect(
testImage,
Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.drawImageRect(
testImage,
Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.drawImageRect(
testImage,
Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.clipRect(const 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),
SurfacePaint(),
);
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final 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(const 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),
SurfacePaint(),
);
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final 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(const 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),
SurfacePaint(),
);
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.restore();
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
rc.clipRRect(RRect.fromLTRBR(75, 75, 160, 160, const Radius.circular(5)));
rc.drawImageRect(
testImage,
Rect.fromLTRB(0, 0, testWidth, testHeight),
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight),
SurfacePaint(),
);
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.restore();
await canvasScreenshot(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();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final 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),
SurfacePaint(),
);
rc.drawCircle(
const Offset(100, 100),
50.0,
SurfacePaint()
..strokeWidth = 3
..color = const Color.fromARGB(128, 0, 0, 0),
);
rc.restore();
await canvasScreenshot(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.
ui_web.debugEmulateFlutterTesterEnvironment = false;
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
rc.save();
final Image testImage = createTestImage();
final double testWidth = testImage.width.toDouble();
final double testHeight = testImage.height.toDouble();
const 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),
const Rect.fromLTRB(100, 100, 200, 200),
SurfacePaint(),
);
rc.drawRect(
const Rect.fromLTWH(50, 50, 100, 200),
SurfacePaint()
..strokeWidth = 3
..color = const Color(0xA0000000),
);
const 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 canvasScreenshot(
rc,
'draw_text_composite_order_below',
region: const Rect.fromLTWH(0, 0, 350, 300),
);
});
// Creates a picture
test('Paints nine slice image', () async {
const Rect region = Rect.fromLTWH(0, 0, 500, 500);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final Canvas canvas = Canvas(recorder, region);
final Image testImage = createNineSliceImage();
canvas.clipRect(const Rect.fromLTWH(0, 0, 420, 200));
canvas.drawImageNine(
testImage,
const Rect.fromLTWH(20, 20, 20, 20),
const Rect.fromLTWH(20, 20, 400, 400),
SurfacePaint(),
);
final Picture picture = recorder.endRecording();
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.addPicture(Offset.zero, picture);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final DomElement sceneElement = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
try {
sceneElement.append(builder.build().webOnlyRootElement!);
domDocument.body!.append(sceneElement);
await matchGoldenFile('draw_nine_slice.png', region: region);
} finally {
// The page is reused across tests, so remove the element after taking the
// 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 {
const Rect region = Rect.fromLTWH(0, 0, 100, 100);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final Canvas canvas = Canvas(recorder, region);
final Image testImage = createNineSliceImage();
canvas.clipRect(const 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,
const Rect.fromLTWH(20, 20, 20, 20),
const Rect.fromLTWH(20, 20, 36, 36),
SurfacePaint(),
);
final Picture picture = recorder.endRecording();
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.addPicture(Offset.zero, picture);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final DomElement sceneElement = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
try {
sceneElement.append(builder.build().webOnlyRootElement!);
domDocument.body!.append(sceneElement);
await matchGoldenFile('draw_nine_slice_empty_center.png', region: region);
} finally {
// The page is reused across tests, so remove the element after taking the
// 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 {
const Rect region = 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, SurfacePaint());
await canvasScreenshot(canvas, 'draw_clipped_and_transformed_image', region: region);
});
/// Regression test for https://github.com/flutter/flutter/issues/61245
test('Should render image with perspective', () async {
const Rect region = Rect.fromLTRB(0, 0, 200, 200);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.translate(10, 10);
canvas.drawImage(createTestImage(), Offset.zero, SurfacePaint());
final Matrix4 transform =
Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.0005); // perspective
canvas.transform(transform.storage);
canvas.drawImage(createTestImage(), const Offset(0, 100), SurfacePaint());
await canvasScreenshot(canvas, 'draw_3d_image', region: region, setupPerspective: true);
});
/// Regression test for https://github.com/flutter/flutter/issues/61245
test('Should render image with perspective inside clip area', () async {
const Rect region = Rect.fromLTRB(0, 0, 200, 200);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, SurfacePaint()..color = const Color(0xFFE0E0E0));
canvas.translate(10, 10);
canvas.drawImage(createTestImage(), Offset.zero, SurfacePaint());
final Matrix4 transform =
Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.0005); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 100, 200),
SurfacePaint()..color = const Color(0x801080E0),
);
canvas.drawImage(createTestImage(), const Offset(0, 100), SurfacePaint());
canvas.drawRect(
const Rect.fromLTWH(50, 150, 50, 20),
SurfacePaint()..color = const Color(0x80000000),
);
await canvasScreenshot(canvas, 'draw_3d_image_clipped', region: region, setupPerspective: true);
});
test('Should render rect with perspective transform', () async {
const Rect region = Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, SurfacePaint()..color = const Color(0xFFE0E0E0));
canvas.translate(20, 20);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 100, 40),
SurfacePaint()..color = const Color(0xFF000000),
);
final Matrix4 transform =
Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(
const Rect.fromLTWH(0, 60, 120, 40),
SurfacePaint()..color = const Color(0x801080E0),
);
canvas.drawRect(
const Rect.fromLTWH(300, 250, 120, 40),
SurfacePaint()..color = const Color(0x80E010E0),
);
canvas.drawRRect(
RRect.fromRectAndRadius(const Rect.fromLTWH(0, 120, 160, 40), const Radius.circular(5)),
SurfacePaint()..color = const Color(0x801080E0),
);
canvas.drawRRect(
RRect.fromRectAndRadius(const Rect.fromLTWH(300, 320, 90, 40), const Radius.circular(20)),
SurfacePaint()..color = const Color(0x80E010E0),
);
await canvasScreenshot(canvas, 'draw_3d_rect_clipped', region: region, setupPerspective: true);
});
test('Should render color and ovals with perspective transform', () async {
const Rect region = Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, SurfacePaint()..color = const Color(0xFFFF0000));
canvas.drawColor(const Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 100, 40),
SurfacePaint()..color = const Color(0xFF000000),
);
final Matrix4 transform =
Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawOval(
const Rect.fromLTWH(0, 120, 130, 40),
SurfacePaint()..color = const Color(0x801080E0),
);
canvas.drawOval(
const Rect.fromLTWH(300, 290, 90, 40),
SurfacePaint()..color = const Color(0x80E010E0),
);
canvas.drawCircle(const Offset(60, 240), 50, SurfacePaint()..color = const Color(0x801080E0));
canvas.drawCircle(const Offset(360, 370), 30, SurfacePaint()..color = const Color(0x80E010E0));
await canvasScreenshot(canvas, 'draw_3d_oval_clipped', region: region, setupPerspective: true);
});
test('Should render path with perspective transform', () async {
const Rect region = Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, SurfacePaint()..color = const Color(0xFFFF0000));
canvas.drawColor(const Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 100, 20),
SurfacePaint()..color = const Color(0xFF000000),
);
final Matrix4 transform =
Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.drawRect(
const Rect.fromLTWH(0, 120, 130, 40),
SurfacePaint()..color = const Color(0x801080E0),
);
canvas.drawOval(
const Rect.fromLTWH(300, 290, 90, 40),
SurfacePaint()..color = const Color(0x80E010E0),
);
final Path path = Path();
path.moveTo(50, 50);
path.lineTo(100, 50);
path.lineTo(100, 100);
path.close();
canvas.drawPath(path, SurfacePaint()..color = const Color(0x801080E0));
canvas.drawCircle(const Offset(50, 50), 4, SurfacePaint()..color = const Color(0xFF000000));
canvas.drawCircle(const Offset(100, 100), 4, SurfacePaint()..color = const Color(0xFF000000));
canvas.drawCircle(const Offset(100, 50), 4, SurfacePaint()..color = const Color(0xFF000000));
await canvasScreenshot(canvas, 'draw_3d_path', region: region, setupPerspective: true);
});
test('Should render path with perspective transform', () async {
const Rect region = Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, SurfacePaint()..color = const Color(0xFFFF0000));
canvas.drawColor(const Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(
const Rect.fromLTWH(0, 0, 100, 20),
SurfacePaint()..color = const Color(0xFF000000),
);
final Matrix4 transform =
Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
//canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(
const Rect.fromLTWH(0, 120, 130, 40),
SurfacePaint()..color = const Color(0x801080E0),
);
canvas.drawOval(
const Rect.fromLTWH(300, 290, 90, 40),
SurfacePaint()..color = const Color(0x80E010E0),
);
final Path path = Path();
path.moveTo(50, 50);
path.lineTo(100, 50);
path.lineTo(100, 100);
path.close();
canvas.drawPath(path, SurfacePaint()..color = const Color(0x801080E0));
canvas.drawCircle(const Offset(50, 50), 4, SurfacePaint()..color = const Color(0xFF000000));
canvas.drawCircle(const Offset(100, 100), 4, SurfacePaint()..color = const Color(0xFF000000));
canvas.drawCircle(const Offset(100, 50), 4, SurfacePaint()..color = const Color(0xFF000000));
await canvasScreenshot(canvas, 'draw_3d_path_clipped', region: region, setupPerspective: true);
});
}
// 9 slice test image that has a shiny/glass look.
const String base64PngData =
'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'
'pAt0EUj0LiYQRNFBxnISs2L/00YRa6NkwHYAdsCLoBqpx7V5WjuTWZEMrat5faIvBgjxjRb'
'ptG+IsKjJ6ToU5UTZLFdPENaXUL+4gNqFi1iuMT8/jcb8AtqkfBxHQNREVG8CzTaLbbaJGJ'
'KKlCApQCkRCCHXELycvyMx2VyWawUI8jnEhQIJnEEuk0U2F6AyOopCdQil/ipKQwMsV+GrH'
'9UqabdJjAAZzuWLABukdREWgB5hSjSRR4py0fq5aUwdfxFTzzyDsz9+FrOPfR9NLIIoot2Z'
'XMsIAeUSeL1zybWpvaMMxr2QdY1RUh89SkYI5qobl5ln+JDsKO89hNFbb8Tm91+P7e+9Hps'
'm9qBQLiNutZAE0vFUCtn1kuStZ7SEcGgUyyBi/tpTT+HFb3wTk1/7KuqcQgBnc2UEWzfD6+'
'9HJiBlCZokQhSWCIbkaIbzrKRU3AwBa+6gwQaSl91Y55iI3JFeKgkOgo6oIyVeo4XW0hzCU1'
'MkdkqUYQxj3xc+jet+5R4M7tlt3M6SST6ZtF5ajTAXSAhs1A4RZTy0KarP/O3f4Sd/+Gksc3'
'Rx/FrkByrItglio4FQFK0vI9PiuM4/P6KeCxjqcIa8T/wM5yTYFAOhLNSka8SG/YigyRgBZ'
'LNPmfBi9ucEIl7skf8Udb2LcszzeVI6i7DAvEBRZvvykRfQYr7pPdfjji98Hrs+eJidfWS'
'yGseRtiA7dNIqhEXRiIaACoX6wgU8+eW/xgtf+kuUt4wjPzyIzMISsFgzg5TJS9cyCKg3YC'
'6xFzdowmwNQ81LBVPlkDoiQiipn9W4XtBtVRvnYJtgpDLxl/NJ9yU1VK+wSQvdjii6DUQt2g'
'zCkAz2o03k54++aCL/kUe+hQN3302KUZcFX8c2aF2lVQgL2TBu09408fRX/gY//LPPoXLgAI'
'pL5OTcIjIUt6BYQEA9aS/MIbk4Y3oqJkmAhJ7AdA+LloSA3qdcTHVbZSVxx6UURVdL+7ua7I'
'HGxPz1N29DQm5HFHEZSEljuGkAzZ+d5HzL+NgTj2P/bXcgCVuENTAVc/N0ERZ3k4hL+hGe'
'feQRPPHJ30bfwQMoTC8gaDSJaAl+u4Vw5hS5QvHGZhQPvxulrVuR3TSKbLmIbDGPDAHJUI'
'yzfLiaGT0ZvtinFSWnZJGFmCVxU1RSYqNHNUgS8pY6G4qjXC+hpW8vN9BcmEfz7HnUJ09h6'
'cWnTIdt3NZxtGln2st1tLaOoH7iZTNsn3j+WWzet5/UpS0hHC51EY6ikJIc4OzRo/jmDdcDW'
'8ZRlvI3G0SmBO/kKzbRtt/8HYzdfhjl8TEUaLBy+QK8XJ6IkPJEJvYFuf6rLN6yxP3bF/9FVN'
'HUkDahVsVgERH06Fc7hDp6FEuVI4q1CBHRZoRUqcbsBUwdewlnHv4Ozr3wY2B4K9qUunB+Ccn'
'EGM4fO4J9n/wEPvqlv0Kxr2xwS02VDGFxty0LWW/gH//i8zjy5a9g9F0H4J2aRXagD94bL2P'
'gF2/FdZ96AJsOXY+gr4iWWBNSwORgaPNn0jwCPwV8bSFFTP0um9h1ZR4irYp++Lj9Wos0Z8/'
'jlSe+h6N//lkkpVE0Kjk0l+po7tiGaSJ936OP4roP30N1Ipwdq20Ia+OOyd3Xf/IMvn34ZpR'
'370d2oYY8N/XM1AmM3H433vfQZ9A3MU7Pjtok4yEgmDLCziCy6srPem0rby9T4qRdqnW6SjS'
'6RXJcHCvSIaERO/kvT+KpT/0eGpu3o91oo1XKYe7MSQzdchPu//o3UN60CVnBw8f4LP5EyzW'
'89Njjphs5ikeWRPDZVkIfbvj9P0Dfrp2o1SgyNAQJrV9Cj8jn9uDTG5LeasvQk2GbPTkSa9WT'
'RcAxAdvWe1b37dgCN5fNW7C1wPF+Nk8YfLTqNdQpmdtvvxPv+tPPwj83hWy1j/ksqvt/Aad/'
'8COcePbZVNU6BAu0X3o0MLOnT+O1L34RA9vGgLMzFOV+RJOvYNdDf4zKnmvRpEUsFqgPpKwM'
'SkxDIXeuVadehU3zcvw2jY30je9lLELqnSRfap0ktBHGto5odLglQybeaf+XcZMPrZ3EV5kup'
'lW05Yl4lDgvTycnyCMfpMRLWnRhucbE7bfh1D/cjOnnfojc5jE6KEvgbo3nHvt77D98C/J9VE'
'3CRjOq7d/HGz990RzFUb7IzNW57yU2YHTvPhLEQ5YTty7OYuH0edROT2HxzHksT0/TanNrWp'
'xDe47cX2zS11623FukJCR8QPE357PWQXF1JvRTYSUHUWaN+zslIdNHJ7JShkfxDCpFeCznRk'
'eQHxlFfvMI+rdsQ9+27SiMDFm/fHUE2266CTNEOEvCtJeX0Ld1DCe/+jVMPfAA9hy6zvyHQK'
'Itakz++w8ovtRJcjJDKnr1OnJEeenlE4j4YbA4OYmZ53+Kucef4t57qrt/Sie0RyoJcO23ZA'
'vLFD0CH5M7Cbnho2rt9lqdzNCZL9SxwtSvkO5YXIe45l2gpFxIZ'
'+6VCdNBDtdGk9/3Pgx/4L0YPXgQxf4BxPQM5WuH3MpydNKSoQriM8Dkc89j97sPmsGjfPhYmj'
'mLcw8/TiM1iICdNalPDicDI3j16w8jnJqhDz1tiGWE0vZxUrFIv5nOIMXEo5RojMRX24dhTgd'
'eX1zypyW06cceO61JtLv2jjuQ9dIuZS3K9dCpIOrpO9vCKJE0nK1aDc3jT2ORz0nOWdq6D+0'
'cR1a2GNPkoVFfDa43/uMZ1D92H8qVCrzlZjM5+q//hu/e9SEM7NqNwlwNOQJugAj2ptx0cmKE'
'nfWJSD2FfGhZa+k/ERK3TCyJtRx/NbHRxqm0cXJ9lKcIC71VqbdK5ZauewFdWT6g6CbcqCOq'
'UXxhiW2UzCLbOEEzDtEqF7AwfQFLYQ2/+9JRXLN7F4KQYnTu1RO2TBDkKJ+LhDoVnFgISI8'
'oBUmDulhr2GQihv6TiUysSWytlKKZtkpuV5J7v9KikuvjciG9Ura+a6pqk1GM6SiBHqDsix'
'm3kQF4VAUdPEgyJH0+9+T87u04f/w4zk9NYsvuCaoNnY25o/9psi+vxp1U2MRCih/1ssgxRV'
'WeU8JHhDAR5Hvxtlvu1CWAaut9tG/31n+uMtcXHInEXfPSxoDISuJc0geKT8Low1X6fvZnL5'
'sP4dcWFzFNdpO39JWlv5yB/7splc9utbcgz8rkd02flWV7e7/9ZVu3A7syfiSa/y+Ep0+8h'
'naL3wR1OuW1J/8J5YBWtNmyTdph3MV7DUIOdHsv+VHq9LncmLTz2/TrQOnAI//eq9Vt95k5'
'dgx17kZ+/eK8MdTbOgyPCK/i7tsE19s9rSO6T5H36AbnCv1Y+NHT/MZfhL8wP297aoZfPT6'
'3pKspybhm6GsHg1V+Zc3y0HERwdJSymFRQ6ZW1s1R6J2MvFxWJV9+An1xmrVUpBsLC7YRyD'
'lQuhqQNTy0XxtGxEkWneXaInUYy/R9WdnALnWGvDOzlIVyQ7lFEYUGz+T8Fo9GVPG43zqKv'
'DPRuxRq4SM+K5f/0tYu1KYVU0VOR/d8ifWrIUmP0zPu9CRVx1h+yA96S5JpfYhepcmJN7+3'
'13FWr0Kknbr6Olq1REvmDuOuJnx1iZd+oBIrougnvJW7WpPEWB8yxNl2oozO4YKgkH6B6'
'VqMMSZedj6HDRL/EMK1Xoq9VgEV2e8gm7YqmEIMtjD6WYp4q6W9WxTIo6aSPyXElyJtD1de'
'PWtrv3byV3c77JsYYTz6o1PF/q5+VfP48vWYm4KWeJsB3h2P6Vyv2Vzt8Lj3ltbOhtu9J5Nu'
'r3VmhnBxHEqT27wKNbHsaXijzbrvJCm6skPPtJdA6s9D8JaTrj/8mvpDWiUQ7jeeQOvAdFHl'
'f55SJv8m+5mZfbPMsyhFNavhmKOvr0jlH553lEITf+rVLL4kt4gC/Xqn9iAkXdgOapyKXde'
'wIVmfIzqZJHHqTXaeSYi7vHu2oredfeqTKFjvJ1CE/H+4Lb/rpGaf5V63XA4cdMwlQfQ7qU'
'LZlsIkQhqriI6VEK6OjdNg8bYiy7uh4vgOzLOR0Qr8Hu6cMffI9XqUXk0OrdKTCJCS/XbKPW'
'+vqNg7f+9J7aWD2ZP/hapbTwRSBEEcMHwjatshXmnnNt5pMToox7vd/p3j4AE974dChDwICH'
'gIL0vtPqJF3bVxVu72sBcAfYAouXEa48rpmyv/dWM1pYU89Qx163SbiK04re8iJ2G6IIh5R'
'h2dImaVKoZ4NROQ2zzSDVBlCEFmUxWtmQWGD+iMORUNi9USsppZTb2Prdbb4Lb3NE/dOQL'
'AwQY0+7u2NBeQnXcu5wLuncRT45TU1ru4DuN7m9z8UhuVI74nrS0sohW3UPrQHRgYHGa8V6'
'B7Yg+VkWEUb72LFwoXKAZB96xZYuGe9HaPQAgQW9Atqjxti7WQTheUs9mdReuGSO9c3eVS'
'IYmry1172ldzpHOZlLCsddJHc6fv3Fpaz82j4xydnbfZh2E4GDnIOJUKr1HpSfoBbxWK1'
'KKNxwy6oQy4zzUtkGUnbjzRMwveQRshygmSoZAR3/Y7g7bDZGefqZfHOsQdHnaznFr+ro1'
'uvkaWLowchxjewymNnfYBu+PtdFu2stwK7qVijgKsgzry+eKGL12L86igIhnXHGO1lp3uy'
'Y3HLFhkiymfpm68j+XdD/M1eCSvXAV5j39LxnX02394trJ0l5SPr0JeXqjWK7WuTMofeTDG'
'LyG16q6jKf08nM4QIk36iOMxqnc/xsI52bQYrBKm8FmERUw1UHd/W3wEFpxSAtJPJVbvdN'
'9d62dfpfMs712SgnLPKOHWwar3JIfrYTRg82Y7R40SZV2sywh/7BIV6g826F6qbDeeR4E5e'
'rVjB00wcs5KHNOK2Yoq29zMSL3I50t0R0bPJVeSqC6bv/rbIIm65lcNH6RXrYpliz9kAJjT'
'cmkf2lG7HlwCEUc4ot40U7CcCditsQvZH+fBmje/bi9Md/HfVHHkXmGsY/zTUQkHLupNr7f'
'3bo1dk/bEvSNiaDGRLGNiP0xN2xe+/F8PAoSgqmoxcp48eQKSJM2S4WixjkCf3YnXebwVI8'
'FDWd5l2UpMUU9Yh8Wk7F14mSxOmyj8Tuv3s2msPGCIbV4yPqjcGinC8l4gpQi4YraJycRPH'
'u7Cd3O1jjFmRCGfMi5RUE2FVioxXLPNrYnRiJzY9+CAWF2YQ9pWIPENEOaFy3YFbmYuEsUSe'
'Wxj3git5Qo3Z4Iloztefg3rKdwqUSccShs4cEY1lyMjUFoNR236W8doeWoMlLE/xI4jE2Xnf'
'r6I6NESEKwz8YfgFtyRjLt9ZIS+xrvRjqbKI8cO3YplUmnv0O8iN74Q/v8xbt4YFbsu8amB'
'qxVlTiM6VpMt16xj71VOlQmsxWRIhCarrR5ui9sjur+maDJQZ2dvkzf8CJj73JxjZuxvV/g'
'rKjKPuPai070EhoEi1EsW6f6CKBm/Wd37013Bsjlep//x9ZHdsZ1QNY6Pma3bJbArCMRrH'
'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+k0AAAAAyBMj8AAAAAElFTkSuQmCC';
const String base64ImageUrl = 'data:image/png;base64,$base64PngData';
HtmlImage createNineSliceImage() {
return HtmlImage(createDomHTMLImageElement()..src = base64ImageUrl, 60, 60);
}
HtmlImage createTestImage({int width = 100, int height = 50}) {
final DomCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D 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();
final DomHTMLImageElement imageElement = createDomHTMLImageElement();
imageElement.src = js_util.callMethod<String>(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();
}

View File

@@ -1,131 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../../common/test_initialization.dart';
import '../screenshot.dart';
const Rect region = Rect.fromLTWH(0, 0, 500, 100);
void main() {
internalBootstrapBrowserTest(() => testMain);
}
SurfacePaint makePaint() => Paint() as SurfacePaint;
Future<void> testMain() async {
setUpUnitTests(emulateTesterEnvironment: false, setUpTestViewDimensions: false);
setUpAll(() async {
debugShowClipLayers = true;
});
setUp(() async {
SurfaceSceneBuilder.debugForgetFrameScene();
});
group('Add picture to scene', () {
test('draw growing picture across frames', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(const Rect.fromLTRB(0, 0, 100, 100));
_drawTestPicture(builder, 100, false);
builder.pop();
final DomElement elm1 = builder.build().webOnlyRootElement!;
domDocument.body!.append(elm1);
// Now draw picture again but at larger size.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushClipRect(const Rect.fromLTRB(0, 0, 100, 100));
// Now draw the picture at original target size, which will use a
// different code path that should normally not have width/height set
// on image element.
_drawTestPicture(builder2, 20, false);
builder2.pop();
elm1.remove();
await sceneScreenshot(builder2, 'canvas_draw_picture_acrossframes', region: region);
});
test('draw growing picture across frames clipped', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushClipRect(const Rect.fromLTRB(0, 0, 100, 100));
_drawTestPicture(builder, 100, true);
builder.pop();
final DomElement elm1 = builder.build().webOnlyRootElement!;
domDocument.body!.append(elm1);
// Now draw picture again but at larger size.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushClipRect(const Rect.fromLTRB(0, 0, 100, 100));
_drawTestPicture(builder2, 20, true);
builder2.pop();
elm1.remove();
await sceneScreenshot(builder2, 'canvas_draw_picture_acrossframes_clipped', region: region);
});
test('PictureInPicture', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture greenRectPicture = _drawGreenRectIntoPicture();
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 100, 100));
canvas.drawPicture(greenRectPicture);
builder.addPicture(const Offset(10, 10), recorder.endRecording());
await sceneScreenshot(builder, 'canvas_draw_picture_in_picture_rect', region: region);
});
});
}
HtmlImage? sharedImage;
void _drawTestPicture(SceneBuilder builder, double targetSize, bool clipped) {
sharedImage ??= _createRealTestImage();
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 100, 100));
canvas.debugEnforceArbitraryPaint();
if (clipped) {
canvas.clipRRect(RRect.fromLTRBR(0, 0, targetSize, targetSize, const Radius.circular(4)));
}
canvas.drawImageRect(
sharedImage!,
const Rect.fromLTWH(0, 0, 20, 20),
Rect.fromLTWH(0, 0, targetSize, targetSize),
makePaint(),
);
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
}
Picture _drawGreenRectIntoPicture() {
final EnginePictureRecorder recorder = PictureRecorder() as EnginePictureRecorder;
final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 100, 100));
canvas.drawRect(
const Rect.fromLTWH(20, 20, 50, 50),
makePaint()..color = const Color(0xFF00FF00),
);
return recorder.endRecording();
}
const String _base64Encoded20x20TestImage =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA'
'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj'
'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg==';
HtmlImage _createRealTestImage() {
return HtmlImage(
createDomHTMLImageElement()..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage',
20,
20,
);
}

View File

@@ -1,175 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 300, 300);
late BitmapCanvas canvas;
late BitmapCanvas domCanvas;
setUp(() {
canvas = BitmapCanvas(region, RenderStrategy());
// setting isInsideSvgFilterTree true forces use of DOM canvas
domCanvas = BitmapCanvas(region, RenderStrategy()..isInsideSvgFilterTree = true);
});
tearDown(() {
canvas.rootElement.remove();
domCanvas.rootElement.remove();
});
test('draws lines with varying strokeWidth', () async {
paintLines(canvas);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_lines_thickness.png', region: region);
});
test('draws lines with varying strokeWidth with dom canvas', () async {
paintLines(domCanvas);
domDocument.body!.append(domCanvas.rootElement);
await matchGoldenFile('canvas_lines_thickness_dom_canvas.png', region: region);
});
test('draws lines with negative Offset values with dom canvas', () async {
// test rendering lines correctly with negative offset when using DOM
final SurfacePaintData paintWithStyle =
SurfacePaintData()
..color =
0xFFE91E63 // Colors.pink
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
// canvas.drawLine ignores paint.style (defaults to fill) according to api docs.
// expect lines are rendered the same regardless of the set paint.style
final SurfacePaintData paintWithoutStyle =
SurfacePaintData()
..color =
0xFF4CAF50 // Colors.green
..strokeWidth = 16
..strokeCap = StrokeCap.round;
// test vertical, horizontal, and diagonal lines
final List<Offset> points = <Offset>[
const Offset(-25, 50),
const Offset(45, 50),
const Offset(100, -25),
const Offset(100, 200),
const Offset(-150, -145),
const Offset(100, 200),
];
final List<Offset> shiftedPoints =
points.map((Offset point) => point.translate(20, 20)).toList();
paintLinesFromPoints(domCanvas, paintWithStyle, points);
paintLinesFromPoints(domCanvas, paintWithoutStyle, shiftedPoints);
domDocument.body!.append(domCanvas.rootElement);
await matchGoldenFile('canvas_lines_with_negative_offset.png', region: region);
});
test('drawLines method respects strokeCap with dom canvas', () async {
final SurfacePaintData paintStrokeCapRound =
SurfacePaintData()
..color =
0xFFE91E63 // Colors.pink
..strokeWidth = 16
..strokeCap = StrokeCap.round;
final SurfacePaintData paintStrokeCapSquare =
SurfacePaintData()
..color =
0xFF4CAF50 // Colors.green
..strokeWidth = 16
..strokeCap = StrokeCap.square;
final SurfacePaintData paintStrokeCapButt =
SurfacePaintData()
..color =
0xFFFF9800 // Colors.orange
..strokeWidth = 16
..strokeCap = StrokeCap.butt;
// test vertical, horizontal, and diagonal lines
final List<Offset> points = <Offset>[
const Offset(5, 50),
const Offset(45, 50),
const Offset(100, 5),
const Offset(100, 200),
const Offset(5, 10),
const Offset(100, 200),
];
final List<Offset> shiftedPoints =
points.map((Offset point) => point.translate(50, 50)).toList();
final List<Offset> twiceShiftedPoints =
shiftedPoints.map((Offset point) => point.translate(50, 50)).toList();
paintLinesFromPoints(domCanvas, paintStrokeCapRound, points);
paintLinesFromPoints(domCanvas, paintStrokeCapSquare, shiftedPoints);
paintLinesFromPoints(domCanvas, paintStrokeCapButt, twiceShiftedPoints);
domDocument.body!.append(domCanvas.rootElement);
await matchGoldenFile('canvas_lines_with_strokeCap.png', region: region);
});
}
void paintLines(BitmapCanvas canvas) {
final SurfacePaintData nullPaint =
SurfacePaintData()
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final SurfacePaintData paint1 =
SurfacePaintData()
..color =
0xFF9E9E9E // Colors.grey
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final SurfacePaintData paint2 =
SurfacePaintData()
..color = 0x7fff0000
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final SurfacePaintData paint3 =
SurfacePaintData()
..color =
0xFF4CAF50 //Colors.green
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
// Draw markers around 100x100 box
canvas.drawLine(const Offset(50, 40), const Offset(148, 40), nullPaint);
canvas.drawLine(const Offset(50, 50), const Offset(52, 50), paint1);
canvas.drawLine(const Offset(150, 50), const Offset(148, 50), paint1);
canvas.drawLine(const Offset(50, 150), const Offset(52, 150), paint1);
canvas.drawLine(const Offset(150, 150), const Offset(148, 150), paint1);
// Draw diagonal
canvas.drawLine(const Offset(50, 50), const Offset(150, 150), paint2);
// Draw horizontal
paint3.strokeWidth = 1.0;
paint3.color = 0xFFFF0000;
canvas.drawLine(const Offset(50, 55), const Offset(150, 55), paint3);
paint3.strokeWidth = 2.0;
paint3.color = 0xFF2196F3; // Colors.blue;
canvas.drawLine(const Offset(50, 60), const Offset(150, 60), paint3);
paint3.strokeWidth = 4.0;
paint3.color = 0xFFFF9800; // Colors.orange;
canvas.drawLine(const Offset(50, 70), const Offset(150, 70), paint3);
}
void paintLinesFromPoints(BitmapCanvas canvas, SurfacePaintData paint, List<Offset> points) {
// points list contains pairs of Offset points, so for loop step is 2
for (int i = 0; i < points.length - 1; i += 2) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}

View File

@@ -1,74 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 150, 420);
late BitmapCanvas canvas;
setUp(() {
canvas = BitmapCanvas(region, RenderStrategy());
});
tearDown(() {
canvas.rootElement.remove();
});
test('draws rect with flipped coordinates L > R, T > B', () async {
paintRects(canvas);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_rect_flipped.png', region: region);
});
}
void paintRects(BitmapCanvas canvas) {
canvas.drawRect(
const Rect.fromLTRB(30, 40, 100, 50),
SurfacePaintData()
..color =
0xFF4CAF50 //Colors.green
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
// swap left and right.
canvas.drawRect(
const Rect.fromLTRB(100, 150, 30, 140),
SurfacePaintData()
..color =
0xFFF44336 //Colors.red
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
// Repeat above for fill
canvas.drawRect(
const Rect.fromLTRB(30, 240, 100, 250),
SurfacePaintData()
..color =
0xFF4CAF50 //Colors.green
..style = PaintingStyle.fill,
);
// swap left and right.
canvas.drawRect(
const Rect.fromLTRB(100, 350, 30, 340),
SurfacePaintData()
..color =
0xFFF44336 //Colors.red
..style = PaintingStyle.fill,
);
}

View File

@@ -1,119 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
late RecordingCanvas rc;
const Rect canvasRect = Rect.fromLTWH(0, 0, 500, 100);
const Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old golden tester padding
final SurfacePaint niceRRectPaint =
SurfacePaint()
..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(() {
rc = RecordingCanvas(const Rect.fromLTWH(0, 0, 500, 100));
rc.translate(10, 10); // Center
});
test('round square with big (equal) radius ends up as a circle', () async {
for (int i = 0; i < 5; i++) {
rc.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
Radius.circular(rRectRadii[i]),
),
niceRRectPaint,
);
}
await canvasScreenshot(rc, 'canvas_rrect_round_square', canvasRect: canvasRect, region: region);
});
/// Regression test for https://github.com/flutter/flutter/issues/62631
test('round square with flipped left/right coordinates', () async {
rc.translate(35, 320);
rc.drawRRect(
RRect.fromRectAndRadius(const Rect.fromLTRB(-30, -100, 30, -300), const Radius.circular(30)),
niceRRectPaint,
);
rc.drawPath(
Path()
..moveTo(0, 0)
..lineTo(20, 0),
niceRRectPaint,
);
await canvasScreenshot(
rc,
'canvas_rrect_flipped',
canvasRect: canvasRect,
region: const Rect.fromLTWH(0, 0, 100, 200),
);
});
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,
);
rc.drawRRect(rrect, niceRRectPaint);
}
await canvasScreenshot(
rc,
'canvas_rrect_overlapping_radius',
canvasRect: canvasRect,
region: region,
);
});
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,
);
rc.drawDRRect(outerRRect, innerRRect, niceRRectPaint);
}
await canvasScreenshot(
rc,
'canvas_drrect_overlapping_radius',
canvasRect: canvasRect,
region: region,
);
});
}

View File

@@ -1,84 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 300, 300);
late BitmapCanvas canvas;
setUp(() {
canvas = BitmapCanvas(region, RenderStrategy());
});
tearDown(() {
canvas.rootElement.remove();
});
test('draws stroke joins', () async {
paintStrokeJoins(canvas);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_stroke_joins.png', region: region);
});
}
void paintStrokeJoins(BitmapCanvas canvas) {
canvas.drawRect(
const Rect.fromLTRB(0, 0, 300, 300),
SurfacePaintData()
..color = 0xFFFFFFFF
..style = PaintingStyle.fill,
); // white
Offset start = const Offset(20, 10);
Offset mid = const Offset(120, 10);
Offset end = const Offset(120, 20);
final List<StrokeCap> strokeCaps = <StrokeCap>[StrokeCap.butt, StrokeCap.round, StrokeCap.square];
for (final StrokeCap cap in strokeCaps) {
final List<StrokeJoin> joints = <StrokeJoin>[
StrokeJoin.miter,
StrokeJoin.bevel,
StrokeJoin.round,
];
const List<Color> colors = <Color>[
Color(0xFFF44336),
Color(0xFF4CAF50),
Color(0xFF2196F3),
]; // red, green, blue
for (int i = 0; i < joints.length; i++) {
final StrokeJoin join = joints[i];
final Color color = colors[i % colors.length];
final Path path = Path();
path.moveTo(start.dx, start.dy);
path.lineTo(mid.dx, mid.dy);
path.lineTo(end.dx, end.dy);
canvas.drawPath(
path,
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 4
..color = color.value
..strokeJoin = join
..strokeCap = cap,
);
start = start.translate(0, 20);
mid = mid.translate(0, 20);
end = end.translate(0, 20);
}
}
}

View File

@@ -1,77 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 300, 300);
late BitmapCanvas canvas;
setUp(() {
canvas = BitmapCanvas(region, RenderStrategy());
});
tearDown(() {
canvas.rootElement.remove();
});
test('draws rects side by side with fill and stroke', () async {
paintSideBySideRects(canvas);
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('canvas_stroke_rects.png', region: region);
});
}
void paintSideBySideRects(BitmapCanvas canvas) {
canvas.drawRect(
const Rect.fromLTRB(0, 0, 300, 300),
SurfacePaintData()
..color = 0xFFFFFFFF
..style = PaintingStyle.fill,
); // white
canvas.drawRect(
const Rect.fromLTRB(0, 20, 40, 60),
SurfacePaintData()
..style = PaintingStyle.fill
..color = 0x7f0000ff,
);
canvas.drawRect(
const Rect.fromLTRB(40, 20, 80, 60),
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 4
..color = 0x7fff0000,
);
// Rotate 30 degrees (in rad: deg*pi/180)
canvas.transform(Matrix4.rotationZ(30.0 * math.pi / 180.0).storage);
canvas.drawRect(
const Rect.fromLTRB(100, 60, 140, 100),
SurfacePaintData()
..style = PaintingStyle.fill
..color = 0x7fff00ff,
);
canvas.drawRect(
const Rect.fromLTRB(140, 60, 180, 100),
SurfacePaintData()
..style = PaintingStyle.stroke
..strokeWidth = 4
..color = 0x7fffff00,
);
}

View File

@@ -1,99 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old golden tester padding
Future<void> testPath(Path path, String goldenFileName) async {
const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 800);
final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds, RenderStrategy());
final RecordingCanvas canvas = RecordingCanvas(canvasBounds);
SurfacePaint paint =
SurfacePaint()
..color = const Color(0x7F7F7F7F)
..style = PaintingStyle.fill;
canvas.drawPath(path, paint);
paint =
SurfacePaint()
..strokeWidth = 2.0
..color = const Color(0xFF7F007F)
..style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
canvas.endRecording();
domDocument.body!.append(bitmapCanvas.rootElement);
canvas.apply(bitmapCanvas, canvasBounds);
await matchGoldenFile('$goldenFileName.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');
});
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');
});
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');
});
}

View File

@@ -1,139 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
test('rect stroke with clip', () async {
const Rect region = Rect.fromLTWH(0, 0, 250, 250);
// Set `hasParagraphs` to true to force DOM rendering.
final BitmapCanvas canvas = BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
const Rect rect = Rect.fromLTWH(0, 0, 150, 150);
canvas.clipRect(rect.inflate(10.0), ClipOp.intersect);
canvas.drawRect(
rect,
SurfacePaintData()
..color = 0x6fff0000
..strokeWidth = 20.0
..style = PaintingStyle.stroke,
);
canvas.drawRect(
rect,
SurfacePaintData()
..color = 0x6f0000ff
..strokeWidth = 10.0
..style = PaintingStyle.stroke,
);
canvas.drawRect(
rect,
SurfacePaintData()
..color = 0xff000000
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
domDocument.body!.style.margin = '0px';
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('rect_clip_strokes_dom.png', region: region);
canvas.rootElement.remove();
});
test('rrect stroke with clip', () async {
const Rect region = Rect.fromLTWH(0, 0, 250, 250);
// Set `hasParagraphs` to true to force DOM rendering.
final BitmapCanvas canvas = BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
final RRect rrect = RRect.fromRectAndRadius(
const Rect.fromLTWH(0, 0, 150, 150),
const Radius.circular(20),
);
canvas.clipRect(rrect.outerRect.inflate(10.0), ClipOp.intersect);
canvas.drawRRect(
rrect,
SurfacePaintData()
..color = 0x6fff0000
..strokeWidth = 20.0
..style = PaintingStyle.stroke,
);
canvas.drawRRect(
rrect,
SurfacePaintData()
..color = 0x6f0000ff
..strokeWidth = 10.0
..style = PaintingStyle.stroke,
);
canvas.drawRRect(
rrect,
SurfacePaintData()
..color = 0xff000000
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
domDocument.body!.style.margin = '0px';
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('rrect_clip_strokes_dom.png', region: region);
canvas.rootElement.remove();
});
test('circle stroke with clip', () async {
const Rect region = Rect.fromLTWH(0, 0, 250, 250);
// Set `hasParagraphs` to true to force DOM rendering.
final BitmapCanvas canvas = BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
const Rect rect = Rect.fromLTWH(0, 0, 150, 150);
canvas.clipRect(rect.inflate(10.0), ClipOp.intersect);
canvas.drawCircle(
rect.center,
rect.width / 2,
SurfacePaintData()
..color = 0x6fff0000
..strokeWidth = 20.0
..style = PaintingStyle.stroke,
);
canvas.drawCircle(
rect.center,
rect.width / 2,
SurfacePaintData()
..color = 0x6f0000ff
..strokeWidth = 10.0
..style = PaintingStyle.stroke,
);
canvas.drawCircle(
rect.center,
rect.width / 2,
SurfacePaintData()
..color = 0xff000000
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
domDocument.body!.style.margin = '0px';
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('circle_clip_strokes_dom.png', region: region);
canvas.rootElement.remove();
});
}

View File

@@ -1,461 +0,0 @@
// 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:js_util' as js_util;
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide ImageShader, TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
setUpUnitTests(setUpTestViewDimensions: false);
setUp(() {
GlContextCache.dispose();
glRenderer = null;
});
Future<void> testVertices(
String fileName,
Vertices vertices,
BlendMode blendMode,
Paint paint,
) async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
rc.drawVertices(vertices as SurfaceVertices, blendMode, paint as SurfacePaint);
await canvasScreenshot(rc, fileName, canvasRect: screenRect);
}
test('Should draw green hairline triangles when colors array is null.', () async {
final Vertices vertices = Vertices.raw(
VertexMode.triangles,
Float32List.fromList(<double>[
20.0,
20.0,
220.0,
10.0,
110.0,
220.0,
220.0,
320.0,
20.0,
310.0,
200.0,
420.0,
]),
);
await testVertices(
'draw_vertices_hairline_triangle',
vertices,
BlendMode.srcOver,
Paint()..color = const Color.fromARGB(255, 0, 128, 0),
);
});
test('Should draw black hairline triangles when colors array is null'
' and Paint() has no color.', () async {
// ignore: unused_local_variable
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
]);
final Vertices vertices = Vertices.raw(
VertexMode.triangles,
Float32List.fromList(<double>[
20.0,
20.0,
220.0,
10.0,
110.0,
220.0,
220.0,
320.0,
20.0,
310.0,
200.0,
420.0,
]),
);
await testVertices(
'draw_vertices_hairline_triangle_black',
vertices,
BlendMode.srcOver,
Paint(),
);
});
/// Regression test for https://github.com/flutter/flutter/issues/71442.
test(
'Should draw filled triangles when colors array is null'
' and Paint() has color.',
() async {
// ignore: unused_local_variable
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
]);
final Vertices vertices = Vertices.raw(
VertexMode.triangles,
Float32List.fromList(<double>[
20.0,
20.0,
220.0,
10.0,
110.0,
220.0,
220.0,
320.0,
20.0,
310.0,
200.0,
420.0,
]),
);
await testVertices(
'draw_vertices_triangle_green_filled',
vertices,
BlendMode.srcOver,
Paint()
..style = PaintingStyle.fill
..color = const Color(0xFF00FF00),
);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test('Should draw hairline triangleFan.', () async {
final Vertices vertices = Vertices.raw(
VertexMode.triangleFan,
Float32List.fromList(<double>[
150.0,
150.0,
20.0,
10.0,
80.0,
20.0,
220.0,
15.0,
280.0,
30.0,
300.0,
420.0,
]),
);
await testVertices(
'draw_vertices_hairline_triangle_fan',
vertices,
BlendMode.srcOver,
Paint()..color = const Color.fromARGB(255, 0, 128, 0),
);
});
test('Should draw hairline triangleStrip.', () async {
final Vertices vertices = Vertices.raw(
VertexMode.triangleStrip,
Float32List.fromList(<double>[
20.0,
20.0,
220.0,
10.0,
110.0,
220.0,
220.0,
320.0,
20.0,
310.0,
200.0,
420.0,
]),
);
await testVertices(
'draw_vertices_hairline_triangle_strip',
vertices,
BlendMode.srcOver,
Paint()..color = const Color.fromARGB(255, 0, 128, 0),
);
});
test(
'Should draw triangles with colors.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
]);
final Vertices vertices = Vertices.raw(
VertexMode.triangles,
Float32List.fromList(<double>[
150.0,
150.0,
20.0,
10.0,
80.0,
20.0,
220.0,
15.0,
280.0,
30.0,
300.0,
420.0,
]),
colors: colors,
);
await testVertices(
'draw_vertices_triangles',
vertices,
BlendMode.srcOver,
Paint()..color = const Color.fromARGB(255, 0, 128, 0),
);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test(
'Should draw triangles with colors and indices.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF0000FF,
]);
final Uint16List indices = Uint16List.fromList(<int>[0, 1, 2, 3, 4, 0]);
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Vertices vertices = Vertices.raw(
VertexMode.triangles,
Float32List.fromList(<double>[
210.0,
150.0,
30.0,
110.0,
80.0,
30.0,
220.0,
15.0,
280.0,
30.0,
]),
colors: colors,
indices: indices,
);
rc.drawVertices(vertices as SurfaceVertices, BlendMode.srcOver, SurfacePaint());
await canvasScreenshot(rc, 'draw_vertices_triangles_indexed', canvasRect: screenRect);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test(
'Should draw triangleFan with colors.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
]);
final Vertices vertices = Vertices.raw(
VertexMode.triangleFan,
Float32List.fromList(<double>[
150.0,
150.0,
20.0,
10.0,
80.0,
20.0,
220.0,
15.0,
280.0,
30.0,
300.0,
420.0,
]),
colors: colors,
);
await testVertices(
'draw_vertices_triangle_fan',
vertices,
BlendMode.srcOver,
Paint()..color = const Color.fromARGB(255, 0, 128, 0),
);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test(
'Should draw triangleStrip with colors.',
() async {
final Int32List colors = Int32List.fromList(<int>[
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
0xFFFF0000,
0xFF00FF00,
0xFF0000FF,
]);
final Vertices vertices = Vertices.raw(
VertexMode.triangleStrip,
Float32List.fromList(<double>[
20.0,
20.0,
220.0,
10.0,
110.0,
220.0,
220.0,
320.0,
20.0,
310.0,
200.0,
420.0,
]),
colors: colors,
);
await testVertices(
'draw_vertices_triangle_strip',
vertices,
BlendMode.srcOver,
Paint()..color = const Color.fromARGB(255, 0, 128, 0),
);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
Future<void> testTexture(TileMode tileMode, String filename) async {
final Uint16List indices = Uint16List.fromList(<int>[0, 1, 2, 3, 4, 0]);
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Vertices vertices = Vertices.raw(
VertexMode.triangles,
Float32List.fromList(<double>[210.0, 150.0, 0.0, 0.0, 80.0, 30.0, 220.0, 15.0, 280.0, 30.0]),
indices: indices,
);
final Float32List matrix4 = Matrix4.identity().storage;
final HtmlImage img = await createTestImage();
final SurfacePaint paint = SurfacePaint();
final EngineImageShader imgShader = EngineImageShader(
img,
tileMode,
tileMode,
Float64List.fromList(matrix4),
FilterQuality.high,
);
paint.shader = imgShader;
rc.drawVertices(vertices as SurfaceVertices, BlendMode.srcOver, paint);
await canvasScreenshot(rc, filename, canvasRect: screenRect);
expect(imgShader.debugDisposed, false);
imgShader.dispose();
expect(imgShader.debugDisposed, true);
}
test(
'Should draw triangle with texture and indices',
() async {
await testTexture(TileMode.clamp, 'draw_vertices_texture');
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test(
'Should draw triangle with texture and indices',
() async {
await testTexture(TileMode.mirror, 'draw_vertices_texture_mirror');
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test(
'Should draw triangle with texture and indices',
() async {
await testTexture(TileMode.repeated, 'draw_vertices_texture_repeated');
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
}
Future<HtmlImage> createTestImage({int width = 50, int height = 40}) {
final DomCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.fillStyle = '#E04040';
ctx.fillRect(0, 0, width / 3, height);
ctx.fill();
ctx.fillStyle = '#40E080';
ctx.fillRect(width / 3, 0, width / 3, height);
ctx.fill();
ctx.fillStyle = '#2040E0';
ctx.fillRect(2 * width / 3, 0, width / 3, height);
ctx.fill();
final DomHTMLImageElement imageElement = createDomHTMLImageElement();
final Completer<HtmlImage> completer = Completer<HtmlImage>();
imageElement.addEventListener(
'load',
createDomEventListener((DomEvent event) {
completer.complete(HtmlImage(imageElement, width, height));
}),
);
imageElement.src = js_util.callMethod<String>(canvas, 'toDataURL', <dynamic>[]);
return completer.future;
}

View File

@@ -1,181 +0,0 @@
// 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:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Matcher listEqual(List<int> source, {int tolerance = 0}) {
return predicate((List<int> target) {
if (source.length != target.length) {
return false;
}
for (int i = 0; i < source.length; i += 1) {
if ((source[i] - target[i]).abs() > tolerance) {
return false;
}
}
return true;
}, source.toString());
}
// Converts `rawPixels` into a list of bytes that represent raw pixels in rgba8888.
//
// Each element of `rawPixels` represents a bytes in order 0xRRGGBBAA, with
// pixel order Left to right, then top to bottom.
Uint8List _pixelsToBytes(List<int> rawPixels) {
return Uint8List.fromList(<int>[
for (final int pixel in rawPixels) ...<int>[
(pixel >> 24) & 0xff, // r
(pixel >> 16) & 0xff, // g
(pixel >> 8) & 0xff, // b
(pixel >> 0) & 0xff, // a
],
]);
}
Future<Image> _encodeToHtmlThenDecode(
Uint8List rawBytes,
int width,
int height, {
PixelFormat pixelFormat = PixelFormat.rgba8888,
}) async {
final ImageDescriptor descriptor = ImageDescriptor.raw(
await ImmutableBuffer.fromUint8List(rawBytes),
width: width,
height: height,
pixelFormat: pixelFormat,
);
return (await (await descriptor.instantiateCodec()).getNextFrame()).image;
}
// This utility function detects how the current Web engine decodes pixel data.
//
// The HTML renderer uses the BMP format to display pixel data, but it used to
// use a wrong implementation. The bug has been fixed, but the fix breaks apps
// that had to provide incorrect data to work around this issue. This function
// is used in the migration guide to assist libraries that would like to run on
// both pre- and post-patch engines by testing the current behavior on a single
// pixel, making use the fact that the patch fixes the pixel order.
//
// The `format` argument is used for testing. In the actual code it should be
// replaced by `PixelFormat.rgba8888`.
//
// See also:
//
// * Patch: https://github.com/flutter/engine/pull/29448
// * Migration guide: https://docs.flutter.dev/release/breaking-changes/raw-images-on-web-uses-correct-origin-and-colors
Future<bool> rawImageUsesCorrectBehavior(PixelFormat format) async {
final ImageDescriptor descriptor = ImageDescriptor.raw(
await ImmutableBuffer.fromUint8List(Uint8List.fromList(<int>[0xED, 0, 0, 0xFF])),
width: 1,
height: 1,
pixelFormat: format,
);
final Image image = (await (await descriptor.instantiateCodec()).getNextFrame()).image;
final Uint8List resultPixels = Uint8List.sublistView(
(await image.toByteData(format: ImageByteFormat.rawStraightRgba))!,
);
return resultPixels[0] == 0xED;
}
Future<void> testMain() async {
test('Correctly encodes an opaque image', () async {
// A 2x2 testing image without transparency.
final Image sourceImage = await _encodeToHtmlThenDecode(
_pixelsToBytes(<int>[0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00]),
2,
2,
);
final Uint8List actualPixels = Uint8List.sublistView(
(await sourceImage.toByteData(format: ImageByteFormat.rawStraightRgba))!,
);
// The `benchmarkPixels` is identical to `sourceImage` except for the fully
// transparent last pixel, whose channels are turned 0.
final Uint8List benchmarkPixels = _pixelsToBytes(<int>[
0xFF0102FF,
0x04FE05FF,
0x0708FDFF,
0x00000000,
]);
expect(actualPixels, listEqual(benchmarkPixels));
});
test('Correctly encodes an opaque image in bgra8888', () async {
// A 2x2 testing image without transparency.
final Image sourceImage = await _encodeToHtmlThenDecode(
_pixelsToBytes(<int>[0xFF0102FF, 0x04FE05FF, 0x0708FDFF, 0x0A0B0C00]),
2,
2,
pixelFormat: PixelFormat.bgra8888,
);
final Uint8List actualPixels = Uint8List.sublistView(
(await sourceImage.toByteData(format: ImageByteFormat.rawStraightRgba))!,
);
// The `benchmarkPixels` is the same as `sourceImage` except that the R and
// G channels are swapped and the fully transparent last pixel is turned 0.
final Uint8List benchmarkPixels = _pixelsToBytes(<int>[
0x0201FFFF,
0x05FE04FF,
0xFD0807FF,
0x00000000,
]);
expect(actualPixels, listEqual(benchmarkPixels));
});
test('Correctly encodes a transparent image', () async {
// A 2x2 testing image with transparency.
final Image sourceImage = await _encodeToHtmlThenDecode(
_pixelsToBytes(<int>[0xFF800006, 0xFF800080, 0xFF8000C0, 0xFF8000FF]),
2,
2,
);
final Image blueBackground = await _encodeToHtmlThenDecode(
_pixelsToBytes(<int>[0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF]),
2,
2,
);
// The standard way of testing the raw bytes of `sourceImage` is to draw
// the image onto a canvas and fetch its data (see HtmlImage.toByteData).
// But here, we draw an opaque background first before drawing the image,
// and test if the blended result is expected.
//
// This is because, if we only draw the `sourceImage`, the resulting pixels
// will be slightly off from the raw pixels. The reason is unknown, but
// very likely because the canvas.getImageData introduces rounding errors
// if any pixels are left semi-transparent, which might be caused by
// converting to and from pre-multiplied values. See
// https://github.com/flutter/flutter/issues/92958 .
final DomCanvasElement canvas =
createDomCanvasElement()
..width = 2
..height = 2;
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.drawImage((blueBackground as HtmlImage).imgElement, 0, 0);
ctx.drawImage((sourceImage as HtmlImage).imgElement, 0, 0);
final DomImageData imageData = ctx.getImageData(0, 0, 2, 2);
final List<int> actualPixels = imageData.data;
final Uint8List benchmarkPixels = _pixelsToBytes(<int>[
0x0603F9FF,
0x80407FFF,
0xC0603FFF,
0xFF8000FF,
]);
expect(actualPixels, listEqual(benchmarkPixels, tolerance: 1));
});
test('The behavior detector works correctly', () async {
expect(await rawImageUsesCorrectBehavior(PixelFormat.rgba8888), true);
expect(await rawImageUsesCorrectBehavior(PixelFormat.bgra8888), false);
});
}

View File

@@ -1,176 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../common/matchers.dart';
import '../common/test_initialization.dart';
import 'screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
const Color black12Color = Color(0x1F000000);
const Color redAccentColor = Color(0xFFFF1744);
const double kDashLength = 5.0;
setUpUnitTests(setUpTestViewDimensions: false);
test('Should calculate tangent on line', () async {
final Path path = Path();
path.moveTo(50, 130);
path.lineTo(150, 20);
final PathMetric metric = path.computeMetrics().first;
final Tangent t = metric.getTangentForOffset(50.0)!;
expect(t.position.dx, within(from: 83.633, distance: 0.01));
expect(t.position.dy, within(from: 93.0, distance: 0.01));
expect(t.vector.dx, within(from: 0.672, distance: 0.01));
expect(t.vector.dy, within(from: -0.739, distance: 0.01));
});
test('Should calculate tangent on cubic curve', () async {
final Path path = Path();
const double p1x = 240;
const double p1y = 120;
const double p2x = 320;
const double p2y = 25;
path.moveTo(150, 20);
path.quadraticBezierTo(p1x, p1y, p2x, p2y);
final PathMetric metric = path.computeMetrics().first;
final Tangent t = metric.getTangentForOffset(50.0)!;
expect(t.position.dx, within(from: 187.25, distance: 0.01));
expect(t.position.dy, within(from: 53.33, distance: 0.01));
expect(t.vector.dx, within(from: 0.82, distance: 0.01));
expect(t.vector.dy, within(from: 0.56, distance: 0.01));
});
test('Should calculate tangent on quadratic curve', () async {
final Path path = Path();
const double p0x = 150;
const double p0y = 20;
const double p1x = 320;
const double p1y = 25;
path.moveTo(150, 20);
path.quadraticBezierTo(p0x, p0y, p1x, p1y);
final PathMetric metric = path.computeMetrics().first;
final Tangent t = metric.getTangentForOffset(50.0)!;
expect(t.position.dx, within(from: 199.82, distance: 0.01));
expect(t.position.dy, within(from: 21.46, distance: 0.01));
expect(t.vector.dx, within(from: 0.99, distance: 0.01));
expect(t.vector.dy, within(from: 0.02, distance: 0.01));
});
// Test for extractPath to draw 5 pixel length dashed line using quad curve.
test('Should draw dashed line on quadratic curve.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final SurfacePaint paint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = black12Color;
final SurfacePaint redPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = redAccentColor;
final SurfacePath path = SurfacePath();
path.moveTo(50, 130);
path.lineTo(150, 20);
const double p1x = 240;
const double p1y = 120;
const double p2x = 320;
const double p2y = 25;
path.quadraticBezierTo(p1x, p1y, p2x, p2y);
rc.drawPath(path, paint);
const double t0 = 0.2;
const double t1 = 0.7;
final List<PathMetric> metrics = path.computeMetrics().toList();
double totalLength = 0;
for (final PathMetric m in metrics) {
totalLength += m.length;
}
final Path dashedPath = Path();
for (final PathMetric measurePath in path.computeMetrics()) {
double distance = totalLength * t0;
bool draw = true;
while (distance < measurePath.length * t1) {
const double length = kDashLength;
if (draw) {
dashedPath.addPath(measurePath.extractPath(distance, distance + length), Offset.zero);
}
distance += length;
draw = !draw;
}
}
rc.drawPath(dashedPath, redPaint);
await canvasScreenshot(rc, 'path_dash_quadratic', canvasRect: screenRect);
});
// Test for extractPath to draw 5 pixel length dashed line using cubic curve.
test('Should draw dashed line on cubic curve.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final SurfacePaint paint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = black12Color;
final SurfacePaint redPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = redAccentColor;
final Path path = Path();
path.moveTo(50, 130);
path.lineTo(150, 20);
const double p1x = 40;
const double p1y = 120;
const double p2x = 300;
const double p2y = 130;
const double p3x = 320;
const double p3y = 25;
path.cubicTo(p1x, p1y, p2x, p2y, p3x, p3y);
rc.drawPath(path, paint);
const double t0 = 0.2;
const double t1 = 0.7;
final List<PathMetric> metrics = path.computeMetrics().toList();
double totalLength = 0;
for (final PathMetric m in metrics) {
totalLength += m.length;
}
final Path dashedPath = Path();
for (final PathMetric measurePath in path.computeMetrics()) {
double distance = totalLength * t0;
bool draw = true;
while (distance < measurePath.length * t1) {
const double length = kDashLength;
if (draw) {
dashedPath.addPath(measurePath.extractPath(distance, distance + length), Offset.zero);
}
distance += length;
draw = !draw;
}
}
rc.drawPath(dashedPath, redPaint);
await canvasScreenshot(rc, 'path_dash_cubic', canvasRect: screenRect);
});
}

View File

@@ -1,102 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
// TODO(yjbanov): https://github.com/flutter/flutter/issues/76885
const bool issue76885Exists = true;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
test('PathRef.getRRect with radius', () {
final SurfacePath path = SurfacePath();
final RRect rrect = RRect.fromLTRBR(0, 0, 10, 10, const Radius.circular(2));
path.addRRect(rrect);
expect(path.toRoundedRect(), rrect);
});
test('PathRef.getRRect with radius larger than rect', () {
final SurfacePath path = SurfacePath();
final RRect rrect = RRect.fromLTRBR(0, 0, 10, 10, const Radius.circular(20));
path.addRRect(rrect);
expect(
path.toRoundedRect(),
// Path.addRRect will correct the radius to fit the dimensions, so when
// extracting the rrect out of the path we don't get the original.
RRect.fromLTRBR(0, 0, 10, 10, const Radius.circular(5)),
);
});
test('PathRef.getRRect with zero radius', () {
final SurfacePath path = SurfacePath();
final RRect rrect = RRect.fromLTRBR(0, 0, 10, 10, Radius.zero);
path.addRRect(rrect);
expect(path.toRoundedRect(), isNull);
expect(path.toRect(), rrect.outerRect);
});
test('PathRef.getRRect elliptical', () {
final SurfacePath path = SurfacePath();
final RRect rrect = RRect.fromLTRBR(0, 0, 10, 10, const Radius.elliptical(2, 4));
path.addRRect(rrect);
expect(path.toRoundedRect(), rrect);
});
test('PathRef.getRRect elliptical zero x', () {
final SurfacePath path = SurfacePath();
final RRect rrect = RRect.fromLTRBR(0, 0, 10, 10, const Radius.elliptical(0, 3));
path.addRRect(rrect);
expect(path.toRoundedRect(), isNull);
expect(path.toRect(), rrect.outerRect);
});
test('PathRef.getRRect elliptical zero y', () {
final SurfacePath path = SurfacePath();
final RRect rrect = RRect.fromLTRBR(0, 0, 10, 10, const Radius.elliptical(3, 0));
path.addRRect(rrect);
expect(path.toRoundedRect(), isNull);
expect(path.toRect(), rrect.outerRect);
});
test('PathRef.getRect returns a Rect from a valid Path and null otherwise', () {
final SurfacePath path = SurfacePath();
// Draw a line
path.moveTo(0, 0);
path.lineTo(10, 0);
expect(path.pathRef.getRect(), isNull);
// Draw two other lines to get a valid rectangle
path.lineTo(10, 10);
path.lineTo(0, 10);
expect(path.pathRef.getRect(), const Rect.fromLTWH(0, 0, 10, 10));
});
// Regression test for https://github.com/flutter/flutter/issues/111750
test('PathRef.getRect returns Rect with positive width and height', () {
final SurfacePath path = SurfacePath();
// Draw a rectangle starting from bottom right corner
path.moveTo(10, 10);
path.lineTo(0, 10);
path.lineTo(0, 0);
path.lineTo(10, 0);
// pathRef.getRect() should return a rectangle with positive height and width
expect(path.pathRef.getRect(), const Rect.fromLTWH(0, 0, 10, 10));
});
// This test demonstrates the issue with attempting to reconstruct an RRect
// with imprecision introduced by comparing double values. We should fix this
// by removing the need to reconstruct rrects:
// https://github.com/flutter/flutter/issues/76885
test('PathRef.getRRect with nearly zero corner', () {
final SurfacePath path = SurfacePath();
final RRect original = RRect.fromLTRBR(0, 0, 10, 10, const Radius.elliptical(0.00000001, 5));
path.addRRect(original);
expect(path.toRoundedRect(), original);
}, skip: issue76885Exists);
}

View File

@@ -1,596 +0,0 @@
// 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:js_util' as js_util;
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide window;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('Path', () {
test('Should have no subpaths when created', () {
final SurfacePath path = SurfacePath();
expect(path.isEmpty, isTrue);
});
test('LineTo should add command', () {
final SurfacePath path = SurfacePath();
path.moveTo(5.0, 10.0);
path.lineTo(20.0, 40.0);
path.lineTo(30.0, 50.0);
expect(path.pathRef.countPoints(), 3);
expect(path.pathRef.atPoint(2).dx, 30.0);
expect(path.pathRef.atPoint(2).dy, 50.0);
});
test('LineTo should add moveTo 0,0 when first call to Path API', () {
final SurfacePath path = SurfacePath();
path.lineTo(20.0, 40.0);
expect(path.pathRef.countPoints(), 2);
expect(path.pathRef.atPoint(0).dx, 0);
expect(path.pathRef.atPoint(0).dy, 0);
expect(path.pathRef.atPoint(1).dx, 20.0);
expect(path.pathRef.atPoint(1).dy, 40.0);
});
test('relativeLineTo should increments currentX', () {
final SurfacePath path = SurfacePath();
path.moveTo(5.0, 10.0);
path.lineTo(20.0, 40.0);
path.relativeLineTo(5.0, 5.0);
expect(path.pathRef.countPoints(), 3);
expect(path.pathRef.atPoint(2).dx, 25.0);
expect(path.pathRef.atPoint(2).dy, 45.0);
});
test('Should allow calling relativeLineTo before moveTo', () {
final SurfacePath path = SurfacePath();
path.relativeLineTo(5.0, 5.0);
path.moveTo(5.0, 10.0);
expect(path.pathRef.countPoints(), 3);
expect(path.pathRef.atPoint(1).dx, 5.0);
expect(path.pathRef.atPoint(1).dy, 5.0);
expect(path.pathRef.atPoint(2).dx, 5.0);
expect(path.pathRef.atPoint(2).dy, 10.0);
});
test('Should allow relativeLineTo after reset', () {
final SurfacePath path = SurfacePath();
final Path subPath = Path();
subPath.moveTo(50.0, 60.0);
subPath.lineTo(200.0, 200.0);
path.extendWithPath(subPath, Offset.zero);
path.reset();
path.relativeLineTo(5.0, 5.0);
expect(path.pathRef.countPoints(), 2);
expect(path.pathRef.atPoint(0).dx, 0);
expect(path.pathRef.atPoint(0).dy, 0);
expect(path.pathRef.atPoint(1).dx, 5.0);
});
test('Should detect rectangular path', () {
final SurfacePath path = SurfacePath();
path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0));
expect(path.toRect(), const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0));
});
test('Should detect horizontal line path', () {
SurfacePath path = SurfacePath();
path.moveTo(10, 20);
path.lineTo(100, 0);
expect(path.toStraightLine(), null);
path = SurfacePath();
path.moveTo(10, 20);
path.lineTo(200, 20);
final Rect r = path.toStraightLine()!;
expect(r, equals(const Rect.fromLTRB(10, 20, 200, 20)));
});
test('Should detect vertical line path', () {
final SurfacePath path = SurfacePath();
path.moveTo(10, 20);
path.lineTo(10, 200);
final Rect r = path.toStraightLine()!;
expect(r, equals(const Rect.fromLTRB(10, 20, 10, 200)));
});
test('Should detect non rectangular path if empty', () {
final SurfacePath path = SurfacePath();
expect(path.toRect(), null);
});
test('Should detect non rectangular path if there are multiple subpaths', () {
final SurfacePath path = SurfacePath();
path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0));
path.addRect(const Rect.fromLTWH(5.0, 6.0, 7.0, 8.0));
expect(path.toRect(), null);
});
test('Should detect rounded rectangular path', () {
final SurfacePath path = SurfacePath();
path.addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTRB(1.0, 2.0, 30.0, 40.0),
const Radius.circular(2.0),
),
);
expect(
path.toRoundedRect(),
RRect.fromRectAndRadius(
const Rect.fromLTRB(1.0, 2.0, 30.0, 40.0),
const Radius.circular(2.0),
),
);
});
test('Should detect non rounded rectangular path if empty', () {
final SurfacePath path = SurfacePath();
expect(path.toRoundedRect(), null);
});
test('Should detect rectangular path is not round', () {
final SurfacePath path = SurfacePath();
path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0));
expect(path.toRoundedRect(), null);
});
test('Should detect non rounded rectangular path if there are '
'multiple subpaths', () {
final SurfacePath path = SurfacePath();
path.addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0),
const Radius.circular(2.0),
),
);
path.addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0),
const Radius.circular(2.0),
),
);
expect(path.toRoundedRect(), null);
});
test('Should compute bounds as empty for empty and moveTo only path', () {
final Path emptyPath = Path();
expect(emptyPath.getBounds(), Rect.zero);
final SurfacePath path = SurfacePath();
path.moveTo(50, 60);
expect(path.getBounds(), const Rect.fromLTRB(50, 60, 50, 60));
});
test('Should compute bounds for multiple addRect calls', () {
final Path emptyPath = Path();
expect(emptyPath.getBounds(), Rect.zero);
final SurfacePath path = SurfacePath();
path.addRect(const Rect.fromLTWH(0, 0, 270, 45));
path.addRect(const Rect.fromLTWH(134.5, 0, 1, 45));
expect(path.getBounds(), const Rect.fromLTRB(0, 0, 270, 45));
});
test('Should compute bounds for addRRect', () {
SurfacePath path = SurfacePath();
const Rect bounds = Rect.fromLTRB(30, 40, 400, 300);
RRect rrect = RRect.fromRectAndCorners(
bounds,
topLeft: const Radius.elliptical(1, 2),
topRight: const Radius.elliptical(3, 4),
bottomLeft: const Radius.elliptical(5, 6),
bottomRight: const Radius.elliptical(7, 8),
);
path.addRRect(rrect);
expect(path.getBounds(), bounds);
expect(path.toRoundedRect(), rrect);
path = SurfacePath();
rrect = RRect.fromRectAndCorners(
bounds,
topLeft: const Radius.elliptical(0, 2),
topRight: const Radius.elliptical(3, 4),
bottomLeft: const Radius.elliptical(5, 6),
bottomRight: const Radius.elliptical(7, 8),
);
path.addRRect(rrect);
expect(path.getBounds(), bounds);
expect(path.toRoundedRect(), rrect);
path = SurfacePath();
rrect = RRect.fromRectAndCorners(
bounds,
topRight: const Radius.elliptical(3, 4),
bottomLeft: const Radius.elliptical(5, 6),
bottomRight: const Radius.elliptical(7, 8),
);
path.addRRect(rrect);
expect(path.getBounds(), bounds);
expect(path.toRoundedRect(), rrect);
path = SurfacePath();
rrect = RRect.fromRectAndCorners(
bounds,
topLeft: const Radius.elliptical(1, 2),
bottomLeft: const Radius.elliptical(5, 6),
bottomRight: const Radius.elliptical(7, 8),
);
path.addRRect(rrect);
expect(path.getBounds(), bounds);
expect(path.toRoundedRect(), rrect);
path = SurfacePath();
rrect = RRect.fromRectAndCorners(
bounds,
topLeft: const Radius.elliptical(1, 2),
topRight: const Radius.elliptical(3, 4),
bottomRight: const Radius.elliptical(7, 8),
);
path.addRRect(rrect);
expect(path.getBounds(), bounds);
expect(path.toRoundedRect(), rrect);
path = SurfacePath();
rrect = RRect.fromRectAndCorners(
bounds,
topLeft: const Radius.elliptical(1, 2),
topRight: const Radius.elliptical(3, 4),
bottomLeft: const Radius.elliptical(5, 6),
);
path.addRRect(rrect);
expect(path.getBounds(), bounds);
expect(path.toRoundedRect(), rrect);
});
test('Should compute bounds for lines', () {
final SurfacePath path = SurfacePath();
path.moveTo(25, 30);
path.lineTo(100, 200);
expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200));
final SurfacePath path2 = SurfacePath();
path2.moveTo(250, 300);
path2.lineTo(50, 60);
expect(path2.getBounds(), const Rect.fromLTRB(50, 60, 250, 300));
});
test('Should compute bounds for polygon', () {
final SurfacePath path = SurfacePath();
path.addPolygon(const <Offset>[
Offset(50, 100),
Offset(250, 100),
Offset(152, 180),
Offset(159, 200),
Offset(151, 190),
], true);
expect(path.getBounds(), const Rect.fromLTRB(50, 100, 250, 200));
});
test('Should compute bounds for quadraticBezierTo', () {
final SurfacePath path1 = SurfacePath();
path1.moveTo(285.2, 682.1);
path1.quadraticBezierTo(432.0, 431.4, 594.9, 681.2);
expect(
path1.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(285.2, 556.5, 594.9, 682.1)),
);
// Control point below start , end.
final SurfacePath path2 = SurfacePath();
path2.moveTo(285.2, 682.1);
path2.quadraticBezierTo(447.4, 946.8, 594.9, 681.2);
expect(
path2.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(285.2, 681.2, 594.9, 814.2)),
);
// Control point to the right of end point.
final SurfacePath path3 = SurfacePath();
path3.moveTo(468.3, 685.6);
path3.quadraticBezierTo(644.7, 555.2, 594.9, 681.2);
expect(
path3.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(468.3, 619.3, 605.9, 685.6)),
);
});
test('Should compute bounds for cubicTo', () {
final SurfacePath path1 = SurfacePath();
path1.moveTo(220, 300);
path1.cubicTo(230, 120, 400, 125, 410, 280);
expect(
path1.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 164.3, 410.0, 300.0)),
);
// control point 1 to the right of control point 2
final SurfacePath path2 = SurfacePath();
path2.moveTo(220, 300);
path2.cubicTo(564.2, 13.7, 400.0, 125.0, 410.0, 280.0);
expect(
path2.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 122.8, 440.5, 300.0)),
);
// control point 1 to the right of control point 2 inflection
final SurfacePath path3 = SurfacePath();
path3.moveTo(220, 300);
path3.cubicTo(839.8, 67.9, 400.0, 125.0, 410.0, 280.0);
expect(
path3.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 144.5, 552.1, 300.0)),
);
// control point 1 below and between start and end points
final SurfacePath path4 = SurfacePath();
path4.moveTo(220.0, 300.0);
path4.cubicTo(354.8, 388.3, 400.0, 125.0, 410.0, 280.0);
expect(
path4.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 230.0, 410.0, 318.6)),
);
// control points inverted below
final SurfacePath path5 = SurfacePath();
path5.moveTo(220.0, 300.0);
path5.cubicTo(366.5, 487.3, 256.4, 489.9, 410.0, 280.0);
expect(
path5.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0)),
);
// control points inverted below wide
final SurfacePath path6 = SurfacePath();
path6.moveTo(220.0, 300.0);
path6.cubicTo(496.1, 485.5, 121.4, 491.6, 410.0, 280.0);
expect(
path6.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0)),
);
// control point 2 and end point swapped
final SurfacePath path7 = SurfacePath();
path7.moveTo(220.0, 300.0);
path7.cubicTo(230.0, 120.0, 394.5, 296.1, 382.3, 124.1);
expect(
path7.getBounds(),
within<Rect>(distance: 0.1, from: const Rect.fromLTRB(220.0, 124.1, 382.9, 300.0)),
);
});
// Regression test for https://github.com/flutter/flutter/issues/46813.
test('Should deep copy path', () {
final SurfacePath path = SurfacePath();
path.moveTo(25, 30);
path.lineTo(100, 200);
expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200));
final SurfacePath path2 = SurfacePath.from(path);
path2.lineTo(250, 300);
expect(path2.getBounds(), const Rect.fromLTRB(25, 30, 250, 300));
// Expect original path to stay the same.
expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200));
});
test('Should handle contains inclusive right,bottom coordinates', () {
final Path path = Path();
path.moveTo(50, 60);
path.lineTo(110, 60);
path.lineTo(110, 190);
path.lineTo(50, 190);
path.close();
expect(path.contains(const Offset(80, 190)), isTrue);
expect(path.contains(const Offset(110, 80)), isTrue);
expect(path.contains(const Offset(110, 190)), isTrue);
expect(path.contains(const Offset(110, 191)), isFalse);
});
test('Should not contain top-left of beveled border', () {
final Path path = Path();
path.moveTo(10, 25);
path.lineTo(15, 20);
path.lineTo(25, 20);
path.lineTo(30, 25);
path.lineTo(30, 35);
path.lineTo(25, 40);
path.lineTo(15, 40);
path.lineTo(10, 35);
path.close();
expect(path.contains(const Offset(10, 20)), isFalse);
});
test('Computes contains for cubic curves', () {
final Path path = Path();
path.moveTo(10, 25);
path.cubicTo(10, 20, 10, 20, 20, 15);
path.lineTo(25, 20);
path.cubicTo(30, 20, 30, 20, 30, 25);
path.lineTo(30, 35);
path.cubicTo(30, 40, 30, 40, 25, 40);
path.lineTo(15, 40);
path.cubicTo(10, 40, 10, 40, 10, 35);
path.close();
expect(path.contains(const Offset(10, 20)), isFalse);
expect(path.contains(const Offset(30, 40)), isFalse);
});
// Regression test for https://github.com/flutter/flutter/issues/44470
test(
'Should handle contains for devicepixelratio != 1.0',
() {
js_util.setProperty(domWindow, 'devicePixelRatio', 4.0);
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(4.0);
final Path path =
Path()
..moveTo(50, 0)
..lineTo(100, 100)
..lineTo(0, 100)
..lineTo(50, 0)
..close();
expect(path.contains(const Offset(50, 50)), isTrue);
js_util.setProperty(domWindow, 'devicePixelRatio', 1.0);
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(1.0);
// TODO(ferhat): Investigate failure on CI. Locally this passes.
// [Exception... "Failure" nsresult: "0x80004005 (NS_ERROR_FAILURE)"
},
skip: ui_web.browser.browserEngine == ui_web.BrowserEngine.firefox,
);
// Path contains should handle case where invalid RRect with large
// corner radius is used for hit test. Use case is a RenderPhysicalShape
// with a clipper that contains RRect of width/height 50 but corner radius
// of 100.
//
// Regression test for https://github.com/flutter/flutter/issues/48887
test('Should hit test correctly for malformed rrect', () {
// Correctly formed rrect.
final Path path1 =
Path()..addRRect(RRect.fromLTRBR(50, 50, 100, 100, const Radius.circular(20)));
expect(path1.contains(const Offset(75, 75)), isTrue);
expect(path1.contains(const Offset(52, 75)), isTrue);
expect(path1.contains(const Offset(50, 50)), isFalse);
expect(path1.contains(const Offset(100, 50)), isFalse);
expect(path1.contains(const Offset(100, 100)), isFalse);
expect(path1.contains(const Offset(50, 100)), isFalse);
final Path path2 =
Path()..addRRect(RRect.fromLTRBR(50, 50, 100, 100, const Radius.circular(100)));
expect(path2.contains(const Offset(75, 75)), isTrue);
expect(path2.contains(const Offset(52, 75)), isTrue);
expect(path2.contains(const Offset(50, 50)), isFalse);
expect(path2.contains(const Offset(100, 50)), isFalse);
expect(path2.contains(const Offset(100, 100)), isFalse);
expect(path2.contains(const Offset(50, 100)), isFalse);
});
test('Should set segment masks', () {
final SurfacePath path = SurfacePath();
path.pathRef.computeSegmentMask();
expect(path.pathRef.segmentMasks, 0);
path.moveTo(20, 40);
path.pathRef.computeSegmentMask();
expect(path.pathRef.segmentMasks, 0);
path.lineTo(200, 40);
path.pathRef.computeSegmentMask();
expect(path.pathRef.segmentMasks, SPathSegmentMask.kLine_SkPathSegmentMask);
});
test('Should convert conic to quad when approximation error is small', () {
final Conic conic = Conic(
120.0,
20.0,
160.99470420829266,
20.0,
190.19301120261332,
34.38770865870253,
0.9252691032413082,
);
expect(conic.toQuads().length, 3);
});
test('Should be able to construct from empty path', () {
final SurfacePath path = SurfacePath();
expect(path.isEmpty, isTrue);
final SurfacePath path2 = SurfacePath.from(path);
expect(path2.isEmpty, isTrue);
});
});
group('PathRef', () {
test('Should return empty when created', () {
final PathRef pathRef = PathRef();
expect(pathRef.isEmpty, isTrue);
});
test('Should return non-empty when mutated', () {
final PathRef pathRef = PathRef();
pathRef.growForVerb(SPath.kMoveVerb, 0);
expect(pathRef.isEmpty, isFalse);
});
});
group('PathRefIterator', () {
test('Should iterate through empty path', () {
final Float32List points = Float32List(20);
final PathRef pathRef = PathRef();
final PathRefIterator iter = PathRefIterator(pathRef);
expect(iter.next(points), SPath.kDoneVerb);
});
test('Should iterate through verbs', () {
final Float32List points = Float32List(20);
final PathRef pathRef = PathRef();
pathRef.growForVerb(SPath.kMoveVerb, 0);
pathRef.growForVerb(SPath.kLineVerb, 0);
pathRef.growForVerb(SPath.kQuadVerb, 0);
pathRef.growForVerb(SPath.kCubicVerb, 0);
pathRef.growForVerb(SPath.kConicVerb, 0.8);
pathRef.growForVerb(SPath.kLineVerb, 0.8);
final PathRefIterator iter = PathRefIterator(pathRef);
expect(iter.next(points), SPath.kMoveVerb);
expect(iter.next(points), SPath.kLineVerb);
expect(iter.next(points), SPath.kQuadVerb);
expect(iter.next(points), SPath.kCubicVerb);
expect(iter.next(points), SPath.kConicVerb);
expect(iter.next(points), SPath.kLineVerb);
expect(iter.next(points), SPath.kDoneVerb);
});
test('Should iterate by index through empty path', () {
final PathRef pathRef = PathRef();
final PathRefIterator iter = PathRefIterator(pathRef);
expect(iter.nextIndex(), SPath.kDoneVerb);
});
test('Should iterate through contours', () {
final PathRef pathRef = PathRef();
pathRef.growForVerb(SPath.kMoveVerb, 0);
pathRef.growForVerb(SPath.kLineVerb, 0);
pathRef.growForVerb(SPath.kQuadVerb, 0);
pathRef.growForVerb(SPath.kCubicVerb, 0);
pathRef.growForVerb(SPath.kMoveVerb, 0);
pathRef.growForVerb(SPath.kConicVerb, 0.8);
pathRef.growForVerb(SPath.kLineVerb, 0.8);
pathRef.growForVerb(SPath.kCloseVerb, 0.8);
pathRef.growForVerb(SPath.kMoveVerb, 0);
pathRef.growForVerb(SPath.kLineVerb, 0);
pathRef.growForVerb(SPath.kLineVerb, 0);
final PathRefIterator iter = PathRefIterator(pathRef);
int start = iter.pointIndex;
int end = iter.skipToNextContour();
expect(end - start, 7);
start = end;
end = iter.skipToNextContour();
expect(end - start, 4);
start = end;
end = iter.skipToNextContour();
expect(end - start, 3);
start = end;
end = iter.skipToNextContour();
expect(start, end);
});
/// Regression test for https://github.com/flutter/flutter/issues/68702.
test('Path should return correct bounds after transform', () {
final Path path1 =
Path()
..moveTo(100, 100)
..lineTo(200, 100)
..lineTo(150, 200)
..close();
final SurfacePath path2 = Path.from(path1) as SurfacePath;
final Rect bounds = path2.pathRef.getBounds();
final SurfacePath transformedPath = path2.transform(
Matrix4.identity().scaled(0.5, 0.5).toFloat64(),
);
expect(transformedPath.pathRef.getBounds(), isNot(bounds));
});
});
}

View File

@@ -1,229 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
enum PaintMode { kStrokeAndFill, kStroke, kFill, kStrokeWidthOnly }
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(8, 8, 600, 400); // Compensate for old golden tester padding
Future<void> testPath(
Path path,
String goldenFileName, {
SurfacePaint? paint,
PaintMode mode = PaintMode.kStrokeAndFill,
}) async {
const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 400);
final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds, RenderStrategy());
final RecordingCanvas canvas = RecordingCanvas(canvasBounds);
final bool enableFill = mode == PaintMode.kStrokeAndFill || mode == PaintMode.kFill;
if (enableFill) {
paint ??=
SurfacePaint()
..color = const Color(0x807F7F7F)
..style = PaintingStyle.fill;
canvas.drawPath(path, paint);
}
if (mode == PaintMode.kStrokeAndFill || mode == PaintMode.kStroke) {
paint =
SurfacePaint()
..strokeWidth = 2
..color = enableFill ? const Color(0xFFFF0000) : const Color(0xFF000000)
..style = PaintingStyle.stroke;
}
if (mode == PaintMode.kStrokeWidthOnly) {
paint =
SurfacePaint()
..color = const Color(0xFF4060E0)
..strokeWidth = 10;
}
canvas.drawPath(path, paint!);
final DomElement svgElement = pathToSvgElement(path, paint, enableFill);
canvas.endRecording();
canvas.apply(bitmapCanvas, canvasBounds);
final DomElement sceneElement = createDomElement('flt-scene');
domDocument.body!.append(sceneElement);
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
sceneElement.append(bitmapCanvas.rootElement);
sceneElement.append(svgElement);
await matchGoldenFile('$goldenFileName.png', region: region);
bitmapCanvas.rootElement.remove();
svgElement.remove();
}
tearDown(() {
domDocument.body!.clearChildren();
});
test('render line strokes', () async {
final Path path = Path();
path.moveTo(50, 60);
path.lineTo(200, 300);
await testPath(
path,
'svg_stroke_line',
paint:
SurfacePaint()
..color = const Color(0xFFFF0000)
..strokeWidth = 2.0
..style = PaintingStyle.stroke,
);
});
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');
});
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');
});
test('render arcs', () async {
final List<ArcSample> arcs = <ArcSample>[
ArcSample(Offset.zero, distance: 20),
ArcSample(const Offset(200, 0), largeArc: true, distance: 20),
ArcSample(Offset.zero, clockwise: true, distance: 20),
ArcSample(const Offset(200, 0), largeArc: true, clockwise: true, distance: 20),
ArcSample(Offset.zero, distance: -20),
ArcSample(const Offset(200, 0), largeArc: true, distance: -20),
ArcSample(Offset.zero, clockwise: true, distance: -20),
ArcSample(const Offset(200, 0), largeArc: true, clockwise: true, distance: -20),
];
int sampleIndex = 0;
for (final ArcSample sample in arcs) {
++sampleIndex;
final Path path = sample.createPath();
await testPath(path, 'svg_arc_$sampleIndex');
}
});
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');
});
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), clockwise: false);
path.lineTo(200, 7.8);
path.lineTo(200, 80);
path.lineTo(0, 80);
path.lineTo(0, 10);
await testPath(path, 'svg_notch');
});
/// Regression test for https://github.com/flutter/flutter/issues/70980
test('render notch', () async {
const double w = 0.7;
final Path path = Path();
path.moveTo(0.5, 14);
path.conicTo(0.5, 10.5, 4, 10.5, w);
path.moveTo(4, 10.5);
path.lineTo(6.5, 10.5);
path.moveTo(36.0, 10.5);
path.lineTo(158, 10.5);
path.conicTo(161.5, 10.5, 161.5, 14, w);
path.moveTo(161.5, 14);
path.lineTo(161.5, 48);
path.conicTo(161.5, 51.5, 158, 51.5, w);
path.lineTo(4, 51.5);
path.conicTo(0.5, 51.5, 0.5, 48, w);
path.lineTo(0.5, 14);
await testPath(path, 'svg_editoutline', mode: PaintMode.kStroke);
});
/// Regression test for https://github.com/flutter/flutter/issues/74416
test('render stroke', () async {
final Path path = Path();
path.moveTo(20, 20);
path.lineTo(200, 200);
await testPath(path, 'svg_stroke_width', mode: PaintMode.kStrokeWidthOnly);
});
}
DomElement pathToSvgElement(Path path, Paint paint, bool enableFill) {
final Rect bounds = path.getBounds();
final SVGSVGElement root = createSVGSVGElement();
root.style.transform = 'translate(200px, 0px)';
root.setAttribute('viewBox', '0 0 ${bounds.right} ${bounds.bottom}');
root.width!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, bounds.right);
root.height!.baseVal!.newValueSpecifiedUnits(svgLengthTypeNumber, bounds.bottom);
final SVGPathElement pathElement = createSVGPathElement();
root.append(pathElement);
if (paint.style == PaintingStyle.stroke || paint.strokeWidth != 0.0) {
pathElement.setAttribute('stroke', paint.color.toCssString());
pathElement.setAttribute('stroke-width', paint.strokeWidth);
if (!enableFill) {
pathElement.setAttribute('fill', 'none');
}
}
if (paint.style == PaintingStyle.fill) {
pathElement.setAttribute('fill', paint.color.toCssString());
}
pathElement.setAttribute(
'd',
pathToSvg((path as SurfacePath).pathRef),
); // This is what we're testing!
return root;
}
class ArcSample {
ArcSample(this.offset, {this.largeArc = false, this.clockwise = false, this.distance = 0});
final Offset offset;
final bool largeArc;
final bool clockwise;
final double distance;
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;
}
}

View File

@@ -1,202 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../common/test_initialization.dart';
import 'screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
setUpUnitTests(setUpTestViewDimensions: false);
test('Should draw transformed line.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Path path = Path();
path.moveTo(0, 0);
path.lineTo(300, 200);
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
final Path transformedPath = Path();
final Matrix4 testMatrixTranslateRotate = Matrix4.rotationZ(math.pi * 30.0 / 180.0)
..translate(100, 20);
transformedPath.addPath(path, Offset.zero, matrix4: testMatrixTranslateRotate.toFloat64());
rc.drawPath(
transformedPath,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color.fromRGBO(0, 128, 255, 1.0),
);
await canvasScreenshot(rc, 'path_transform_with_line', canvasRect: screenRect);
});
test('Should draw transformed line.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Path path = Path();
path.addRect(const Rect.fromLTRB(50, 40, 300, 100));
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
final Path transformedPath = Path();
final Matrix4 testMatrixTranslateRotate = Matrix4.rotationZ(math.pi * 30.0 / 180.0)
..translate(100, 20);
transformedPath.addPath(path, Offset.zero, matrix4: testMatrixTranslateRotate.toFloat64());
rc.drawPath(
transformedPath,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color.fromRGBO(0, 128, 255, 1.0),
);
await canvasScreenshot(rc, 'path_transform_with_rect', canvasRect: screenRect);
});
test('Should draw transformed quadratic curve.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Path path = Path();
path.moveTo(100, 100);
path.quadraticBezierTo(100, 300, 400, 300);
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
final Path transformedPath = Path();
final Matrix4 testMatrixTranslateRotate = Matrix4.rotationZ(math.pi * 30.0 / 180.0)
..translate(100, -80);
transformedPath.addPath(path, Offset.zero, matrix4: testMatrixTranslateRotate.toFloat64());
rc.drawPath(
transformedPath,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color.fromRGBO(0, 128, 255, 1.0),
);
await canvasScreenshot(rc, 'path_transform_with_quadratic_curve', canvasRect: screenRect);
});
test('Should draw transformed conic.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
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 + 100);
path.conicTo(pc.dx, pc.dy + 100, p2.dx, p2.dy + 100, 10);
path.close();
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
final Path transformedPath = Path();
final Matrix4 testMatrixTranslateRotate = Matrix4.rotationZ(math.pi * 30.0 / 180.0)
..translate(100, -80);
transformedPath.addPath(path, Offset.zero, matrix4: testMatrixTranslateRotate.toFloat64());
rc.drawPath(
transformedPath,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color.fromRGBO(0, 128, 255, 1.0),
);
await canvasScreenshot(rc, 'path_transform_with_conic', canvasRect: screenRect);
});
test('Should draw transformed arc.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Path path = Path();
path.moveTo(350, 280);
path.arcToPoint(
const Offset(450, 90),
radius: const Radius.elliptical(200, 50),
rotation: -math.pi / 6.0,
largeArc: true,
);
path.close();
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
final Path transformedPath = Path();
final Matrix4 testMatrixTranslateRotate = Matrix4.rotationZ(math.pi * 30.0 / 180.0)
..translate(100, 10);
transformedPath.addPath(path, Offset.zero, matrix4: testMatrixTranslateRotate.toFloat64());
rc.drawPath(
transformedPath,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color.fromRGBO(0, 128, 255, 1.0),
);
await canvasScreenshot(rc, 'path_transform_with_arc', canvasRect: screenRect);
});
test('Should draw transformed rrect.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final Path path = Path();
path.addRRect(RRect.fromLTRBR(50, 50, 300, 200, const Radius.elliptical(4, 8)));
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
final Path transformedPath = Path();
final Matrix4 testMatrixTranslateRotate = Matrix4.rotationZ(math.pi * 30.0 / 180.0)
..translate(100, -80);
transformedPath.addPath(path, Offset.zero, matrix4: testMatrixTranslateRotate.toFloat64());
rc.drawPath(
transformedPath,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color.fromRGBO(0, 128, 255, 1.0),
);
await canvasScreenshot(rc, 'path_transform_with_rrect', canvasRect: screenRect);
});
}

View File

@@ -1,44 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('Picture', () {
test('toImage produces an image', () async {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(
const ui.Rect.fromLTRB(0, 0, 200, 100),
);
canvas.drawCircle(
const ui.Offset(100, 50),
40,
SurfacePaint()..color = const ui.Color.fromARGB(255, 255, 100, 100),
);
final ui.Picture picture = recorder.endRecording();
final HtmlImage image = await picture.toImage(200, 100) as HtmlImage;
expect(image, isNotNull);
domDocument.body!
..style.margin = '0'
..append(image.imgElement);
try {
await matchGoldenFile(
'picture_to_image.png',
region: const ui.Rect.fromLTRB(0, 0, 200, 100),
);
} finally {
image.imgElement.remove();
}
});
});
}

View File

@@ -1,777 +0,0 @@
// 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 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide ColorSpace;
import 'package:ui/ui.dart' hide TextStyle;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import 'package:web_engine_tester/golden_tester.dart';
import '../common/matchers.dart';
import '../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false);
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
final SurfacePaint testPaint = SurfacePaint()..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, RenderStrategy());
// Draws the estimated bounds so we can spot the bug in Gold.
engineCanvas
..save()
..drawRect(
rc.pictureBounds!,
SurfacePaintData()
..color = const Color.fromRGBO(0, 0, 255, 1.0).value
..style = PaintingStyle.stroke
..strokeWidth = 1.0,
)
..restore();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final DomElement sceneElement = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
try {
sceneElement.append(engineCanvas.rootElement);
domDocument.body!.append(sceneElement);
await matchGoldenFile('paint_bounds_for_$fileName.png', region: region);
} finally {
// The page is reused across tests, so remove the element after taking the
// screenshot.
sceneElement.remove();
}
}
test('Empty canvas reports correct paint bounds', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400));
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
// The off by one is due to the minimum stroke width of 1.
expect(rc.pictureBounds, 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);
rc.endRecording();
// The off by one is due to the minimum stroke width of 1.
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, screenWidth, screenHeight));
rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(-200, -100, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
// The extra 0.7 is due to stroke width of 1 rotated by 45 degrees.
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(
rc.pictureBounds,
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);
rc.endRecording();
expect(
rc.pictureBounds,
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 Float32List matrix = Float32List(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);
rc.endRecording();
expect(
rc.pictureBounds,
within(distance: 0.001, from: 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);
rc.endRecording();
expect(rc.pictureBounds, screenRect);
await checkScreenshot(rc, 'draw_paint');
});
test('drawColor should cover full size', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final SurfacePaint testPaint = SurfacePaint()..color = const Color(0xFF80FF00);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.drawColor(const Color(0xFFFF0000), BlendMode.multiply);
rc.drawRect(const Rect.fromLTRB(10, 60, 30, 80), testPaint);
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(rc.pictureBounds, 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,
);
rc.endRecording();
expect(rc.pictureBounds, 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,
);
rc.endRecording();
expect(rc.pictureBounds, Rect.zero);
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,
);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await checkScreenshot(rc, 'draw_drrect');
});
test('Computes paint bounds for draw circle', () async {
// Paint bounds of one circle.
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10.0, 10.0, 30.0, 30.0));
// Paint bounds of a union of two circles.
rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
rc.drawCircle(const Offset(200, 300), 100.0, testPaint);
rc.endRecording();
expect(rc.pictureBounds, 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), SurfacePaint());
rc.endRecording();
expect(rc.pictureBounds, 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),
SurfacePaint(),
);
rc.endRecording();
expect(rc.pictureBounds, 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));
rc.endRecording();
expect(
rc.pictureBounds!.width,
lessThan(widthConstraint),
reason:
'The given width constraint $widthConstraint is more than the '
'test string needs, so the width of the visible text is actually '
'smaller than the given width.',
);
expect(
rc.pictureBounds,
Rect.fromLTRB(textLeft, textTop, textLeft + paragraph.maxIntrinsicWidth, 21.0),
);
await checkScreenshot(rc, 'draw_paragraph');
}, // TODO(mdebbar): https://github.com/flutter/flutter/issues/65789
skip:
ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit &&
ui_web.browser.operatingSystem == ui_web.OperatingSystem.iOs,
);
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;
// Do not go lower than the shortest word.
const double widthConstraint = 130.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
rc.endRecording();
const double fontWidth = 14;
const int lettersInLongestWord = 9;
const double longestLineWidth = lettersInLongestWord * fontWidth;
expect(
rc.pictureBounds!.width,
lessThan(widthConstraint),
reason:
'The test string "A short sentence." is broken up into two lines, '
'"A short" and "sentence.". The longest line contains '
'$lettersInLongestWord characters, each ${fontWidth}px wide. '
'That line is ${longestLineWidth}px wide, which is less than '
'$widthConstraint.',
);
expect(
rc.pictureBounds,
const Rect.fromLTRB(textLeft, textTop, textLeft + longestLineWidth, 35.0),
);
await checkScreenshot(rc, 'draw_paragraph_multi_line');
}, // TODO(mdebbar): https://github.com/flutter/flutter/issues/65789
skip:
ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit &&
ui_web.browser.operatingSystem == ui_web.OperatingSystem.iOs,
);
test('Should exclude painting outside simple clipRect', () async {
// One clipped line.
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
rc.endRecording();
expect(rc.pictureBounds, Rect.zero);
// Two clipped lines.
rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
rc.drawLine(const Offset(52, 53), const Offset(55, 56), testPaint);
rc.endRecording();
// Extra pixel due to default line length
expect(rc.pictureBounds, 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), ClipOp.intersect);
rc.drawRect(const Rect.fromLTRB(20, 60, 120, 70), testPaint);
rc.endRecording();
expect(rc.pictureBounds, 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), ClipOp.intersect);
rc.drawRect(const Rect.fromLTRB(60, 20, 70, 200), testPaint);
rc.endRecording();
expect(rc.pictureBounds, 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), ClipOp.intersect);
rc.scale(2.0, 2.0);
rc.clipRect(const Rect.fromLTRB(30, 30, 45, 45), ClipOp.intersect);
rc.drawRect(const Rect.fromLTRB(10, 30, 60, 35), testPaint);
rc.endRecording();
expect(rc.pictureBounds, 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);
rc.endRecording();
expect(
rc.pictureBounds,
within(distance: 0.05, from: const Rect.fromLTRB(0.0, 8.5, 123.5, 134.1)),
);
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), ClipOp.intersect)
..drawRect(const Rect.fromLTRB(0, 0, 100, 100), SurfacePaint());
rc.endRecording();
expect(rc.pictureBounds, 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), ClipOp.intersect)
..drawRect(const Rect.fromLTWH(-80, -80, 160, 160), SurfacePaint());
rc.endRecording();
expect(
rc.pictureBounds,
within(
distance: 0.001,
from: 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(Offset.zero, const Offset(20, 20), SurfacePaint());
rc.endRecording();
expect(
rc.pictureBounds,
within(distance: 0.1, from: const Rect.fromLTRB(34.4, 48.6, 65.6, 79.7)),
);
await checkScreenshot(rc, 'line_rotated');
});
// Regression test for https://github.com/flutter/flutter/issues/46339.
test('Should draw a Rect for straight line when strokeWidth is zero.', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Path path = Path();
final SurfacePaint paint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 0.0
..color = const Color(0xFFFF0000);
path.moveTo(10, 10);
path.lineTo(90, 10);
rc.drawPath(path, paint);
rc.endRecording();
// Should draw a Rect
final List<PaintCommand> commands = rc.debugPaintCommands;
expect(commands.length, 1);
expect(commands.first, isA<PaintDrawRect>());
// Should inflate picture bounds
expect(rc.pictureBounds, within(distance: 0.1, from: const Rect.fromLTRB(10, 10, 90, 11)));
await checkScreenshot(rc, 'path_straight_line_with_zero_stroke_width');
});
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,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFFFF0000),
);
path.reset();
path.moveTo(0, 3);
path.lineTo(97, 100);
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF00FF00),
);
rc.endRecording();
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,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
rc.restore();
rc.endRecording();
await checkScreenshot(rc, 'path_with_line_and_roundrect');
});
// Regression test for https://github.com/flutter/flutter/issues/64371.
test('Should draw line following a polygon without closing 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.addPolygon(const <Offset>[Offset(0, 10), Offset(20, 5), Offset(50, 10)], false);
path.lineTo(60, 80);
path.lineTo(0, 80);
path.close();
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000),
);
rc.restore();
rc.endRecording();
await checkScreenshot(rc, 'path_with_addpolygon');
});
test('should include paint spread in bounds estimates', () async {
final SurfaceSceneBuilder sb = SurfaceSceneBuilder();
final List<PaintSpreadPainter> painters = <PaintSpreadPainter>[
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawLine(Offset.zero, const Offset(20.0, 20.0), paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawRect(const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0), paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawRRect(RRect.fromLTRBR(0.0, 0.0, 20.0, 20.0, const Radius.circular(7.0)), paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawDRRect(
RRect.fromLTRBR(0.0, 0.0, 20.0, 20.0, const Radius.circular(5.0)),
RRect.fromLTRBR(4.0, 4.0, 16.0, 16.0, const Radius.circular(5.0)),
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawOval(const Rect.fromLTRB(0.0, 5.0, 20.0, 15.0), paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawCircle(const Offset(10.0, 10.0), 10.0, paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
final SurfacePath path =
SurfacePath()
..moveTo(10, 0)
..lineTo(20, 10)
..lineTo(10, 20)
..lineTo(0, 10)
..close();
canvas.drawPath(path, paint);
},
// Images are not affected by mask filter or stroke width. They use image
// filter instead.
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawImage(_createRealTestImage(), Offset.zero, paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawImageRect(
_createRealTestImage(),
const Rect.fromLTRB(0, 0, 20, 20),
const Rect.fromLTRB(5, 5, 15, 15),
paint,
);
},
];
Picture drawBounds(Rect bounds) {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
canvas.drawRect(
bounds,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color.fromARGB(255, 0, 255, 0),
);
return recorder.endRecording();
}
for (int i = 0; i < painters.length; i++) {
sb.pushOffset(0.0, 20.0 + 60.0 * i);
final PaintSpreadPainter painter = painters[i];
// Paint with zero paint spread.
{
sb.pushOffset(20.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint zeroSpreadPaint = SurfacePaint();
painter(canvas, zeroSpreadPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
// Paint with a thick stroke paint.
{
sb.pushOffset(80.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint thickStrokePaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
painter(canvas, thickStrokePaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
// Paint with a mask filter blur.
{
sb.pushOffset(140.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint maskFilterBlurPaint =
SurfacePaint()..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0);
painter(canvas, maskFilterBlurPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
// Paint with a thick stroke paint and a mask filter blur.
{
sb.pushOffset(200.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint thickStrokeAndBlurPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 5.0
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0);
painter(canvas, thickStrokeAndBlurPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
sb.pop();
}
final DomElement sceneElement = sb.build().webOnlyRootElement!;
domDocument.body!.append(sceneElement);
try {
await matchGoldenFile('paint_spread_bounds.png', region: const Rect.fromLTRB(0, 0, 250, 600));
} finally {
sceneElement.remove();
}
});
}
typedef PaintSpreadPainter = void Function(RecordingCanvas canvas, SurfacePaint paint);
const String _base64Encoded20x20TestImage =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUC'
'AIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA'
'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj'
'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg==';
HtmlImage _createRealTestImage() {
return HtmlImage(
createDomHTMLImageElement()..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage',
20,
20,
);
}
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() {}
@override
bool get debugDisposed => false;
@override
Image clone() => this;
@override
bool isCloneOf(Image other) => other == this;
@override
List<StackTrace> /*?*/ debugGetOpenHandleStackTraces() => <StackTrace>[];
@override
ColorSpace get colorSpace => ColorSpace.sRGB;
}
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

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

View File

@@ -1,86 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
final DomElement hostElement = createDomHTMLDivElement();
late DomManager domManager;
late ResourceManager resourceManager;
setUp(() {
domManager = DomManager(devicePixelRatio: 3);
hostElement.appendChild(domManager.rootElement);
resourceManager = ResourceManager(domManager);
});
tearDown(() {
hostElement.clearChildren();
});
test('prepends resources host as sibling to root element (webkit)', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit;
// Resource host hasn't been inserted yet.
expect(
hostElement.children.map((DomElement e) => e.tagName.toLowerCase()),
isNot(contains(ResourceManager.resourcesHostTagName.toLowerCase())),
);
final List<DomElement> resources = <DomElement>[
createDomHTMLDivElement()..setAttribute('test-resource', 'r1'),
createDomHTMLDivElement()..setAttribute('test-resource', 'r2'),
createDomHTMLDivElement()..setAttribute('test-resource', 'r3'),
];
resources.forEach(resourceManager.addResource);
final DomElement resourcesHost = hostElement.firstElementChild!;
expect(resourcesHost.tagName.toLowerCase(), ResourceManager.resourcesHostTagName.toLowerCase());
// Make sure the resources were correctly inserted into the host.
expect(resourcesHost.children, resources);
ui_web.browser.debugBrowserEngineOverride = null;
});
test('prepends resources host inside the shadow root (non-webkit)', () {
ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.blink;
// Resource host hasn't been inserted yet.
expect(
hostElement.children.map((DomElement e) => e.tagName.toLowerCase()),
isNot(contains(ResourceManager.resourcesHostTagName.toLowerCase())),
);
final List<DomElement> resources = <DomElement>[
createDomHTMLDivElement()..setAttribute('test-resource', 'r1'),
createDomHTMLDivElement()..setAttribute('test-resource', 'r2'),
createDomHTMLDivElement()..setAttribute('test-resource', 'r3'),
];
resources.forEach(resourceManager.addResource);
final DomElement resourcesHost = domManager.renderingHost.firstElementChild!;
expect(resourcesHost.tagName.toLowerCase(), ResourceManager.resourcesHostTagName.toLowerCase());
// Make sure the resources were correctly inserted into the host.
expect(resourcesHost.children, resources);
ui_web.browser.debugBrowserEngineOverride = null;
});
test('can remove resource', () {
final DomHTMLDivElement resource = createDomHTMLDivElement();
resourceManager.addResource(resource);
final DomElement? resourceRoot = resource.parent;
expect(resourceRoot, isNotNull);
expect(resourceRoot!.childNodes.length, 1);
resourceManager.removeResource(resource);
expect(resourceRoot.childNodes.length, 0);
});
}

View File

@@ -1,70 +0,0 @@
// 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/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
/// Commit a recording canvas to a bitmap, and compare with the expected.
///
/// [region] specifies the area of the canvas that will be included in the
/// golden.
///
/// If [canvasRect] is omitted, it defaults to the value of [region].
Future<void> canvasScreenshot(
RecordingCanvas rc,
String fileName, {
ui.Rect region = const ui.Rect.fromLTWH(0, 0, 500, 500),
ui.Rect? canvasRect,
bool setupPerspective = false,
}) async {
canvasRect ??= region;
final EngineCanvas engineCanvas = BitmapCanvas(canvasRect, RenderStrategy());
rc.endRecording();
rc.apply(engineCanvas, region);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final DomElement sceneElement = createDomElement('flt-scene');
if (isIosSafari) {
// Shrink to fit on the iPhone screen.
sceneElement.style.position = 'absolute';
sceneElement.style.transformOrigin = '0 0 0';
sceneElement.style.transform = 'scale(0.3)';
}
try {
if (setupPerspective) {
// iFrame disables perspective, set it explicitly for test.
engineCanvas.rootElement.style.perspective = '400px';
for (final DomElement element in engineCanvas.rootElement.querySelectorAll('div')) {
element.style.perspective = '400px';
}
}
sceneElement.append(engineCanvas.rootElement);
domDocument.body!.append(sceneElement);
await matchGoldenFile('$fileName.png', region: region);
} finally {
// The page is reused across tests, so remove the element after taking the
// screenshot.
sceneElement.remove();
}
}
Future<void> sceneScreenshot(
SurfaceSceneBuilder sceneBuilder,
String fileName, {
ui.Rect region = const ui.Rect.fromLTWH(0, 0, 600, 800),
}) async {
DomElement? sceneElement;
try {
sceneElement = sceneBuilder.build().webOnlyRootElement;
domDocument.body!.append(sceneElement!);
await matchGoldenFile('$fileName.png', region: region);
} finally {
// The page is reused across tests, so remove the element after taking the
// screenshot.
sceneElement?.remove();
}
}

View File

@@ -1,921 +0,0 @@
// 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 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../../common/test_initialization.dart';
import '../screenshot.dart';
// TODO(yjbanov): unskip Firefox tests when Firefox implements WebGL in headless mode.
// https://github.com/flutter/flutter/issues/86623
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
const Rect region = Rect.fromLTWH(0, 0, 500, 240);
setUpUnitTests(withImplicitView: true);
test('Paints sweep gradient rectangles', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
GradientSweep sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0,
360.0 / 180.0 * math.pi,
null,
);
final GradientSweep sweepGradientRotated = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0,
360.0 / 180.0 * math.pi,
Matrix4.rotationZ(math.pi / 6.0).storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
// Gradient with default center.
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Gradient with shifted center and rotation.
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
canvas.drawRect(
rectBounds,
SurfacePaint()
..shader = engineGradientToShader(
sweepGradientRotated,
Rect.fromLTWH(
rectBounds.center.dx,
rectBounds.top,
rectBounds.width / 2,
rectBounds.height,
),
),
);
canvas.drawRect(rectBounds, borderPaint);
// Gradient with start/endangle.
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
math.pi / 6,
3 * math.pi / 4,
null,
);
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode repeat
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.repeated,
math.pi / 6,
3 * math.pi / 4,
null,
);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode mirror
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.mirror,
math.pi / 6,
3 * math.pi / 4,
null,
);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
await canvasScreenshot(canvas, 'sweep_gradient_rect', canvasRect: screenRect, region: region);
}, skip: isFirefox);
test('Paints sweep gradient ovals', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
final List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
GradientSweep sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0,
360.0 / 180.0 * math.pi,
null,
);
final GradientSweep sweepGradientRotated = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0,
360.0 / 180.0 * math.pi,
Matrix4.rotationZ(math.pi / 6.0).storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
// Gradient with default center.
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Gradient with shifted center and rotation.
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
canvas.drawOval(
rectBounds,
SurfacePaint()
..shader = engineGradientToShader(
sweepGradientRotated,
Rect.fromLTWH(
rectBounds.center.dx,
rectBounds.top,
rectBounds.width / 2,
rectBounds.height,
),
),
);
canvas.drawRect(rectBounds, borderPaint);
// Gradient with start/endangle.
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
math.pi / 6,
3 * math.pi / 4,
null,
);
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode repeat
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.repeated,
math.pi / 6,
3 * math.pi / 4,
null,
);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode mirror
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.mirror,
math.pi / 6,
3 * math.pi / 4,
null,
);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
await canvasScreenshot(canvas, 'sweep_gradient_oval', canvasRect: screenRect, region: region);
}, skip: isFirefox);
test('Paints sweep gradient paths', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
GradientSweep sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0,
360.0 / 180.0 * math.pi,
null,
);
final GradientSweep sweepGradientRotated = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0,
360.0 / 180.0 * math.pi,
Matrix4.rotationZ(math.pi / 6.0).storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
// Gradient with default center.
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
Path path = samplePathFromRect(rectBounds);
canvas.drawPath(
path,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Gradient with shifted center and rotation.
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
path = samplePathFromRect(rectBounds);
canvas.drawPath(
path,
SurfacePaint()
..shader = engineGradientToShader(
sweepGradientRotated,
Rect.fromLTWH(
rectBounds.center.dx,
rectBounds.top,
rectBounds.width / 2,
rectBounds.height,
),
),
);
canvas.drawRect(rectBounds, borderPaint);
// Gradient with start/endangle.
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
math.pi / 6,
3 * math.pi / 4,
null,
);
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
path = samplePathFromRect(rectBounds);
canvas.drawPath(
path,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode repeat
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.repeated,
math.pi / 6,
3 * math.pi / 4,
null,
);
path = samplePathFromRect(rectBounds);
canvas.drawPath(
path,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode mirror
rectBounds = rectBounds.translate(kBoxWidth + 10, 0);
sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.mirror,
math.pi / 6,
3 * math.pi / 4,
null,
);
path = samplePathFromRect(rectBounds);
canvas.drawPath(
path,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
await canvasScreenshot(canvas, 'sweep_gradient_path', canvasRect: screenRect, region: region);
}, skip: isFirefox);
/// Regression test for https://github.com/flutter/flutter/issues/74137.
test('Paints rotated and shifted linear gradient', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
GradientLinear linearGradient = GradientLinear(
const Offset(50, 50),
const Offset(200, 130),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
// Gradient with default center.
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode repeat
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
linearGradient = GradientLinear(
const Offset(50, 50),
const Offset(200, 130),
colors,
stops,
TileMode.repeated,
Matrix4.identity().storage,
);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
await canvasScreenshot(
canvas,
'linear_gradient_rect_shifted',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
/// Regression test for https://github.com/flutter/flutter/issues/82748.
test('Paints gradient with gradient stop outside range', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[Color(0xFF000000), Color(0xFFFF3C38)];
const List<double> stops = <double>[0.0, 10.0];
final GradientLinear linearGradient = GradientLinear(
const Offset(50, 50),
const Offset(200, 130),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
// Gradient with default center.
const Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
final EngineCanvas engineCanvas = BitmapCanvas(screenRect, RenderStrategy());
canvas.endRecording();
canvas.apply(engineCanvas, screenRect);
}, skip: isFirefox);
test("Creating lots of gradients doesn't create too many webgl contexts", () async {
final DomCanvasElement sideCanvas = createDomCanvasElement(width: 5, height: 5);
final DomCanvasRenderingContextWebGl? context =
sideCanvas.getContext('webgl') as DomCanvasRenderingContextWebGl?;
expect(context, isNotNull);
final EngineCanvas engineCanvas = BitmapCanvas(
const Rect.fromLTRB(0, 0, 100, 100),
RenderStrategy(),
);
for (double x = 0; x < 100; x += 10) {
for (double y = 0; y < 100; y += 10) {
const List<Color> colors = <Color>[Color(0xFFFF0000), Color(0xFF0000FF)];
final GradientLinear linearGradient = GradientLinear(
Offset.zero,
const Offset(10, 10),
colors,
null,
TileMode.clamp,
Matrix4.identity().storage,
);
engineCanvas.drawRect(
Rect.fromLTWH(x, y, 10, 10),
SurfacePaintData()..shader = linearGradient,
);
}
}
expect(context!.isContextLost(), isFalse);
}, skip: isFirefox);
test('Paints clamped, rotated and shifted linear gradient', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
GradientLinear linearGradient = GradientLinear(
const Offset(50, 50),
const Offset(200, 130),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
// Gradient with default center.
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
// Tile mode repeat
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
linearGradient = GradientLinear(
const Offset(50, 50),
const Offset(200, 130),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
await canvasScreenshot(
canvas,
'linear_gradient_rect_clamp_rotated',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
test('Paints linear gradient properly when within svg context', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 240));
canvas.save();
canvas.renderStrategy.isInsideSvgFilterTree = true;
final SurfacePaint borderPaint =
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xFF000000);
const List<Color> colors = <Color>[Color(0xFFFF0000), Color(0xFF0000FF)];
final GradientLinear linearGradient = GradientLinear(
const Offset(125, 75),
const Offset(175, 125),
colors,
null,
TileMode.clamp,
Matrix4.identity().storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 100;
// Gradient with default center.
const Rect rectBounds = Rect.fromLTWH(100, 50, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.drawRect(rectBounds, borderPaint);
canvas.restore();
await canvasScreenshot(
canvas,
'linear_gradient_in_svg_context',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
test('Paints transformed linear gradient', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
final Matrix4 transform =
Matrix4.identity()
..translate(50, 50)
..scale(0.3, 0.7)
..rotateZ(0.5);
final GradientLinear linearGradient = GradientLinear(
const Offset(5, 5),
const Offset(200, 130),
colors,
stops,
TileMode.clamp,
transform.storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds),
);
canvas.restore();
await canvasScreenshot(
canvas,
'linear_gradient_clamp_transformed',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
test('Paints transformed sweep gradient', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
final Matrix4 transform =
Matrix4.identity()
..translate(100, 150)
..scale(0.3, 0.7)
..rotateZ(0.5);
final GradientSweep sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0.0,
2 * math.pi,
transform.storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds),
);
canvas.restore();
await canvasScreenshot(
canvas,
'sweep_gradient_clamp_transformed',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
test('Paints transformed radial gradient', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
final Matrix4 transform =
Matrix4.identity()
..translate(50, 50)
..scale(0.3, 0.7)
..rotateZ(0.5);
final GradientRadial radialGradient = GradientRadial(
const Offset(0.5, 0.5),
400,
colors,
stops,
TileMode.clamp,
transform.storage,
);
const double kBoxWidth = 150;
const double kBoxHeight = 80;
Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
canvas.drawRect(
rectBounds,
SurfacePaint()..shader = engineRadialGradientToShader(radialGradient, rectBounds),
);
rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
canvas.drawOval(
rectBounds,
SurfacePaint()..shader = engineRadialGradientToShader(radialGradient, rectBounds),
);
canvas.restore();
await canvasScreenshot(
canvas,
'radial_gradient_clamp_transformed',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
test('Paints two gradient with same width and different height', () async {
final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
canvas.save();
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
final Matrix4 transform =
Matrix4.identity()
..translate(100, 150)
..scale(0.3, 0.7)
..rotateZ(0.5);
final GradientSweep sweepGradient = GradientSweep(
const Offset(0.5, 0.5),
colors,
stops,
TileMode.clamp,
0.0,
2 * math.pi,
transform.storage,
);
const double kBoxWidth = 150;
const double kBoxHeight1 = 40;
const double kBoxHeight2 = 80;
const Rect rectBounds1 = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight1);
const Rect rectBounds2 = Rect.fromLTWH(10, 80, kBoxWidth, kBoxHeight2);
canvas.drawRect(
rectBounds1,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds1),
);
canvas.drawRect(
rectBounds2,
SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds2),
);
canvas.restore();
await canvasScreenshot(
canvas,
'radial_gradient_double_item',
canvasRect: screenRect,
region: region,
);
}, skip: isFirefox);
}
Shader engineGradientToShader(GradientSweep gradient, Rect rect) {
return Gradient.sweep(
Offset(
rect.left + gradient.center.dx * rect.width,
rect.top + gradient.center.dy * rect.height,
),
gradient.colors,
gradient.colorStops,
gradient.tileMode,
gradient.startAngle,
gradient.endAngle,
gradient.matrix4 == null ? null : Float64List.fromList(gradient.matrix4!),
);
}
Shader engineLinearGradientToShader(GradientLinear gradient, Rect rect) {
return Gradient.linear(
gradient.from,
gradient.to,
gradient.colors,
gradient.colorStops,
gradient.tileMode,
gradient.matrix4 == null ? null : Float64List.fromList(gradient.matrix4!.matrix),
);
}
Shader engineRadialGradientToShader(GradientRadial gradient, Rect rect) {
return Gradient.radial(
Offset(
rect.left + gradient.center.dx * rect.width,
rect.top + gradient.center.dy * rect.height,
),
gradient.radius,
gradient.colors,
gradient.colorStops,
gradient.tileMode,
gradient.matrix4 == null ? null : Float64List.fromList(gradient.matrix4!),
);
}
Path samplePathFromRect(Rect rectBounds) =>
Path()
..moveTo(rectBounds.center.dx, rectBounds.top)
..lineTo(rectBounds.left, rectBounds.bottom)
..quadraticBezierTo(
rectBounds.center.dx + 20,
rectBounds.bottom - 40,
rectBounds.right,
rectBounds.bottom,
)
..close();

View File

@@ -1,137 +0,0 @@
// 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:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
// TODO(yjbanov): unskip Firefox tests when Firefox implements WebGL in headless mode.
// https://github.com/flutter/flutter/issues/86623
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 400.0;
const double screenHeight = 400.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
final HtmlImage testImage = createTestImage();
setUpUnitTests(setUpTestViewDimensions: false);
void drawShapes(RecordingCanvas rc, SurfacePaint paint, Rect shaderRect) {
/// Rect.
rc.drawRect(shaderRect, paint);
shaderRect = shaderRect.translate(100, 0);
/// Circle.
rc.drawCircle(shaderRect.center, shaderRect.width / 2, paint);
shaderRect = shaderRect.translate(110, 0);
/// Oval.
rc.drawOval(
Rect.fromLTWH(shaderRect.left, shaderRect.top, shaderRect.width, shaderRect.height / 2),
paint,
);
shaderRect = shaderRect.translate(-210, 120);
/// Path.
final Path path =
Path()
..moveTo(shaderRect.center.dx, shaderRect.top)
..lineTo(shaderRect.right, shaderRect.bottom)
..lineTo(shaderRect.left, shaderRect.bottom)
..close();
rc.drawPath(path, paint);
shaderRect = shaderRect.translate(100, 0);
/// RRect.
rc.drawRRect(RRect.fromRectXY(shaderRect, 10, 20), paint);
shaderRect = shaderRect.translate(110, 0);
/// DRRect.
rc.drawDRRect(
RRect.fromRectXY(shaderRect, 20, 30),
RRect.fromRectXY(shaderRect.deflate(24), 16, 24),
paint,
);
shaderRect = shaderRect.translate(-200, 120);
}
Future<void> testImageShader(TileMode tmx, TileMode tmy, String fileName) async {
final RecordingCanvas rc = RecordingCanvas(
const Rect.fromLTRB(0, 0, screenWidth, screenHeight),
);
//Rect shaderRect = const Rect.fromLTRB(20, 20, 100, 100);
const Rect shaderRect = Rect.fromLTRB(0, 0, 100, 100);
final SurfacePaint paint = Paint() as SurfacePaint;
paint.shader = ImageShader(
testImage,
tmx,
tmy,
Matrix4.identity().toFloat64(),
filterQuality: FilterQuality.high,
);
drawShapes(rc, paint, shaderRect);
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, fileName, region: screenRect);
}
test('Should draw with tiled imageshader.', () async {
await testImageShader(TileMode.repeated, TileMode.repeated, 'image_shader_tiled');
});
test('Should draw with horizontally mirrored imageshader.', () async {
await testImageShader(TileMode.mirror, TileMode.repeated, 'image_shader_horiz_mirror');
});
test('Should draw with vertically mirrored imageshader.', () async {
await testImageShader(TileMode.repeated, TileMode.mirror, 'image_shader_vert_mirror');
});
test('Should draw with mirrored imageshader.', () async {
await testImageShader(TileMode.mirror, TileMode.mirror, 'image_shader_mirror');
});
test('Should draw with horizontal clamp imageshader.', () async {
await testImageShader(TileMode.clamp, TileMode.repeated, 'image_shader_clamp_horiz');
}, skip: isFirefox);
test('Should draw with vertical clamp imageshader.', () async {
await testImageShader(TileMode.repeated, TileMode.clamp, 'image_shader_clamp_vertical');
}, skip: isFirefox);
test('Should draw with clamp imageshader.', () async {
await testImageShader(TileMode.clamp, TileMode.clamp, 'image_shader_clamp');
}, skip: isFirefox);
}
HtmlImage createTestImage() {
const int width = 16;
const int width2 = width ~/ 2;
const int height = 16;
final DomCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.fillStyle = '#E04040';
ctx.fillRect(0, 0, width2, width2);
ctx.fill();
ctx.fillStyle = '#40E080';
ctx.fillRect(width2, 0, width2, width2);
ctx.fill();
ctx.fillStyle = '#2040E0';
ctx.fillRect(width2, width2, width2, width2);
ctx.fill();
final DomHTMLImageElement imageElement = createDomHTMLImageElement();
imageElement.src = js_util.callMethod<String>(canvas, 'toDataURL', <dynamic>[]);
return HtmlImage(imageElement, width, height);
}

View File

@@ -1,154 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
// TODO(yjbanov): unskip Firefox tests when Firefox implements WebGL in headless mode.
// https://github.com/flutter/flutter/issues/86623
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
test('Should draw linear gradient using rectangle.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
const Rect shaderRect = Rect.fromLTRB(50, 50, 300, 300);
final SurfacePaint paint =
SurfacePaint()
..shader = Gradient.linear(
Offset(shaderRect.left, shaderRect.top),
Offset(shaderRect.right, shaderRect.bottom),
const <Color>[Color(0xFFcfdfd2), Color(0xFF042a85)],
);
rc.drawRect(shaderRect, paint);
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_rect');
});
test('Should blend linear gradient with alpha channel correctly.', () async {
const Rect canvasRect = Rect.fromLTRB(0, 0, 500, 500);
final RecordingCanvas rc = RecordingCanvas(canvasRect);
final SurfacePaint backgroundPaint =
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xFFFF0000);
rc.drawRect(canvasRect, backgroundPaint);
const Rect shaderRect = Rect.fromLTRB(50, 50, 300, 300);
final SurfacePaint paint =
SurfacePaint()
..shader = Gradient.linear(
Offset(shaderRect.left, shaderRect.top),
Offset(shaderRect.right, shaderRect.bottom),
const <Color>[Color(0x00000000), Color(0xFF0000FF)],
);
rc.drawRect(shaderRect, paint);
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_rect_alpha');
});
test('Should draw linear gradient with transform.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final List<double> angles = <double>[0.0, 90.0, 180.0];
double yOffset = 0;
for (final double angle in angles) {
final Rect shaderRect = Rect.fromLTWH(50, 50 + yOffset, 100, 100);
final Matrix4 matrix = Matrix4.identity();
matrix.translate(shaderRect.left, shaderRect.top);
matrix.multiply(Matrix4.rotationZ((angle / 180) * math.pi));
final Matrix4 post = Matrix4.identity();
post.translate(-shaderRect.left, -shaderRect.top);
matrix.multiply(post);
final SurfacePaint paint =
SurfacePaint()
..shader = Gradient.linear(
Offset(shaderRect.left, shaderRect.top),
Offset(shaderRect.right, shaderRect.bottom),
const <Color>[Color(0xFFFF0000), Color(0xFF042a85)],
null,
TileMode.clamp,
matrix.toFloat64(),
);
rc.drawRect(shaderRect, SurfacePaint()..color = const Color(0xFF000000));
rc.drawOval(shaderRect, paint);
yOffset += 120;
}
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_oval_matrix');
}, skip: isFirefox);
// Regression test for https://github.com/flutter/flutter/issues/50010
test('Should draw linear gradient using rounded rect.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
const Rect shaderRect = Rect.fromLTRB(50, 50, 300, 300);
final SurfacePaint paint =
SurfacePaint()
..shader = Gradient.linear(
Offset(shaderRect.left, shaderRect.top),
Offset(shaderRect.right, shaderRect.bottom),
const <Color>[Color(0xFFcfdfd2), Color(0xFF042a85)],
);
rc.drawRRect(RRect.fromRectAndRadius(shaderRect, const Radius.circular(16)), paint);
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_rounded_rect');
});
test('Should draw tiled repeated linear gradient with transform.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final List<double> angles = <double>[0.0, 30.0, 210.0];
double yOffset = 0;
for (final double angle in angles) {
final Rect shaderRect = Rect.fromLTWH(50, 50 + yOffset, 100, 100);
final SurfacePaint paint =
SurfacePaint()
..shader = Gradient.linear(
Offset(shaderRect.left, shaderRect.top),
Offset(shaderRect.left + shaderRect.width / 2, shaderRect.top),
const <Color>[Color(0xFFFF0000), Color(0xFF042a85)],
null,
TileMode.repeated,
Matrix4.rotationZ((angle / 180) * math.pi).toFloat64(),
);
rc.drawRect(shaderRect, SurfacePaint()..color = const Color(0xFF000000));
rc.drawOval(shaderRect, paint);
yOffset += 120;
}
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_tiled_repeated_rect');
}, skip: isFirefox);
test('Should draw tiled mirrored linear gradient with transform.', () async {
final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));
final List<double> angles = <double>[0.0, 30.0, 210.0];
double yOffset = 0;
for (final double angle in angles) {
final Rect shaderRect = Rect.fromLTWH(50, 50 + yOffset, 100, 100);
final SurfacePaint paint =
SurfacePaint()
..shader = Gradient.linear(
Offset(shaderRect.left, shaderRect.top),
Offset(shaderRect.left + shaderRect.width / 2, shaderRect.top),
const <Color>[Color(0xFFFF0000), Color(0xFF042a85)],
null,
TileMode.mirror,
Matrix4.rotationZ((angle / 180) * math.pi).toFloat64(),
);
rc.drawRect(shaderRect, SurfacePaint()..color = const Color(0xFF000000));
rc.drawOval(shaderRect, paint);
yOffset += 120;
}
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_tiled_mirrored_rect');
}, skip: isFirefox);
}

View File

@@ -1,128 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../../common/test_initialization.dart';
import '../screenshot.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
Future<void> testGradient(
String fileName,
Shader shader, {
Rect paintRect = const Rect.fromLTRB(50, 50, 300, 300),
Rect shaderRect = const Rect.fromLTRB(50, 50, 300, 300),
Rect region = const Rect.fromLTWH(0, 0, 500, 500),
}) async {
final RecordingCanvas rc = RecordingCanvas(region);
final SurfacePaint paint = SurfacePaint()..shader = shader;
final Path path = Path();
path.addRect(paintRect);
rc.drawPath(path, paint);
await canvasScreenshot(rc, fileName, region: region);
}
test('Should draw centered radial gradient.', () async {
const Rect shaderRect = Rect.fromLTRB(50, 50, 300, 300);
await testGradient(
'radial_gradient_centered',
Gradient.radial(
Offset((shaderRect.left + shaderRect.right) / 2, (shaderRect.top + shaderRect.bottom) / 2),
shaderRect.width / 2,
<Color>[const Color.fromARGB(255, 0, 0, 0), const Color.fromARGB(255, 0, 0, 255)],
),
);
});
test('Should draw right bottom centered radial gradient.', () async {
const Rect shaderRect = Rect.fromLTRB(50, 50, 300, 300);
await testGradient(
'radial_gradient_right_bottom',
Gradient.radial(Offset(shaderRect.right, shaderRect.bottom), shaderRect.width / 2, <Color>[
const Color.fromARGB(255, 0, 0, 0),
const Color.fromARGB(255, 0, 0, 255),
]),
);
});
test('Should draw with radial gradient with TileMode.clamp.', () async {
const Rect shaderRect = Rect.fromLTRB(50, 50, 100, 100);
await testGradient(
'radial_gradient_tilemode_clamp',
Gradient.radial(
Offset((shaderRect.left + shaderRect.right) / 2, (shaderRect.top + shaderRect.bottom) / 2),
shaderRect.width / 2,
<Color>[const Color.fromARGB(255, 0, 0, 0), const Color.fromARGB(255, 0, 0, 255)],
<double>[0.0, 1.0],
),
shaderRect: shaderRect,
);
});
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> colorStops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
test(
'Should draw with radial gradient with TileMode.repeated.',
() async {
const Rect shaderRect = Rect.fromLTRB(50, 50, 100, 100);
await testGradient(
'radial_gradient_tilemode_repeated',
Gradient.radial(
Offset(
(shaderRect.left + shaderRect.right) / 2,
(shaderRect.top + shaderRect.bottom) / 2,
),
shaderRect.width / 2,
colors,
colorStops,
TileMode.repeated,
),
shaderRect: shaderRect,
region: const Rect.fromLTWH(0, 0, 600, 800),
);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
test(
'Should draw with radial gradient with TileMode.mirrored.',
() async {
const Rect shaderRect = Rect.fromLTRB(50, 50, 100, 100);
await testGradient(
'radial_gradient_tilemode_mirror',
Gradient.radial(
Offset(
(shaderRect.left + shaderRect.right) / 2,
(shaderRect.top + shaderRect.bottom) / 2,
),
shaderRect.width / 2,
colors,
colorStops,
TileMode.mirror,
),
shaderRect: shaderRect,
region: const Rect.fromLTWH(0, 0, 600, 800),
);
},
// TODO(yjbanov): https://github.com/flutter/flutter/issues/86623
skip: isFirefox,
);
}

View File

@@ -1,248 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide BackdropFilterEngineLayer, ClipRectEngineLayer;
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
import '../../common/test_initialization.dart';
/// To debug compositing failures on browsers, set this flag to true and run
/// flutter run -d chrome --web-renderer=html
/// test/golden_tests/engine/shader_mask_golden_test.dart --profile
const bool debugTest = false;
DomElement get sceneHost =>
EnginePlatformDispatcher.instance.implicitView!.dom.renderingHost.querySelector(
DomManager.sceneHostTagName,
)!;
Future<void> main() async {
if (!debugTest) {
internalBootstrapBrowserTest(() => testMain);
} else {
_renderCirclesScene(BlendMode.color);
}
}
// TODO(ferhat): unskip webkit tests once flakiness is resolved. See
// https://github.com/flutter/flutter/issues/76713
// TODO(yjbanov): unskip Firefox tests when Firefox implements WebGL in headless mode.
// https://github.com/flutter/flutter/issues/86623
Future<void> testMain() async {
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
setUpAll(() async {
debugShowClipLayers = true;
});
setUp(() async {
SurfaceSceneBuilder.debugForgetFrameScene();
for (final DomNode scene in sceneHost.querySelectorAll('flt-scene').cast<DomNode>()) {
scene.remove();
}
initWebGl();
});
/// Should render the picture unmodified.
test('Renders shader mask with linear gradient BlendMode dst', () async {
_renderCirclesScene(BlendMode.dst);
await matchGoldenFile('shadermask_linear_dst.png', region: const Rect.fromLTWH(0, 0, 360, 200));
}, skip: isSafari || isFirefox);
/// Should render the gradient only where circles have alpha channel.
test('Renders shader mask with linear gradient BlendMode srcIn', () async {
_renderCirclesScene(BlendMode.srcIn);
await matchGoldenFile(
'shadermask_linear_srcin.png',
region: const Rect.fromLTWH(0, 0, 360, 200),
);
}, skip: isSafari || isFirefox);
test('Renders shader mask with linear gradient BlendMode color', () async {
_renderCirclesScene(BlendMode.color);
await matchGoldenFile(
'shadermask_linear_color.png',
region: const Rect.fromLTWH(0, 0, 360, 200),
);
}, skip: isSafari || isFirefox);
test('Renders shader mask with linear gradient BlendMode xor', () async {
_renderCirclesScene(BlendMode.xor);
await matchGoldenFile('shadermask_linear_xor.png', region: const Rect.fromLTWH(0, 0, 360, 200));
}, skip: isSafari || isFirefox);
test('Renders shader mask with linear gradient BlendMode plus', () async {
_renderCirclesScene(BlendMode.plus);
await matchGoldenFile(
'shadermask_linear_plus.png',
region: const Rect.fromLTWH(0, 0, 360, 200),
);
}, skip: isSafari || isFirefox);
test('Renders shader mask with linear gradient BlendMode modulate', () async {
_renderCirclesScene(BlendMode.modulate);
await matchGoldenFile(
'shadermask_linear_modulate.png',
region: const Rect.fromLTWH(0, 0, 360, 200),
);
}, skip: isSafari || isFirefox);
test('Renders shader mask with linear gradient BlendMode overlay', () async {
_renderCirclesScene(BlendMode.overlay);
await matchGoldenFile(
'shadermask_linear_overlay.png',
region: const Rect.fromLTWH(0, 0, 360, 200),
);
}, skip: isSafari || isFirefox);
/// Should render the gradient opaque on top of content.
test('Renders shader mask with linear gradient BlendMode src', () async {
_renderCirclesScene(BlendMode.src);
await matchGoldenFile('shadermask_linear_src.png', region: const Rect.fromLTWH(0, 0, 360, 200));
}, skip: isSafari || isFirefox);
/// Should render text with gradient.
test('Renders text with linear gradient shader mask', () async {
_renderTextScene(BlendMode.srcIn);
await matchGoldenFile(
'shadermask_linear_text.png',
region: const Rect.fromLTWH(0, 0, 360, 200),
);
}, skip: isSafari || isFirefox);
}
Picture _drawTestPictureWithCircles(Rect region, double offsetX, double offsetY) {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(region);
canvas.drawCircle(
Offset(offsetX + 30, offsetY + 30),
30,
SurfacePaint()..style = PaintingStyle.fill,
);
canvas.drawCircle(
Offset(offsetX + 110, offsetY + 30),
30,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xFFFF0000),
);
canvas.drawCircle(
Offset(offsetX + 30, offsetY + 110),
30,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xFF00FF00),
);
canvas.drawCircle(
Offset(offsetX + 110, offsetY + 110),
30,
SurfacePaint()
..style = PaintingStyle.fill
..color = const Color(0xFF0000FF),
);
return recorder.endRecording();
}
void _renderCirclesScene(BlendMode blendMode) {
const Rect region = Rect.fromLTWH(0, 0, 400, 400);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture circles1 = _drawTestPictureWithCircles(region, 10, 10);
builder.addPicture(Offset.zero, circles1);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
const Rect shaderBounds = Rect.fromLTWH(180, 10, 140, 140);
final EngineGradient shader = GradientLinear(
Offset(200 - shaderBounds.left, 30 - shaderBounds.top),
Offset(320 - shaderBounds.left, 150 - shaderBounds.top),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
builder.pushShaderMask(shader, shaderBounds, blendMode);
final Picture circles2 = _drawTestPictureWithCircles(region, 180, 10);
builder.addPicture(Offset.zero, circles2);
builder.pop();
sceneHost.append(builder.build().webOnlyRootElement!);
}
Picture _drawTestPictureWithText(Rect region, double offsetX, double offsetY) {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(region);
const String text = 'Shader test';
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
fontFamily: 'Roboto',
fontSize: 40.0,
);
final CanvasParagraphBuilder builder = CanvasParagraphBuilder(paragraphStyle);
builder.pushStyle(EngineTextStyle.only(color: const Color(0xFFFF0000)));
builder.addText(text);
final CanvasParagraph paragraph = builder.build();
const double maxWidth = 200 - 10;
paragraph.layout(const ParagraphConstraints(width: maxWidth));
canvas.drawParagraph(paragraph, Offset(offsetX, offsetY));
return recorder.endRecording();
}
void _renderTextScene(BlendMode blendMode) {
const Rect region = Rect.fromLTWH(0, 0, 600, 400);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture textPicture = _drawTestPictureWithText(region, 10, 10);
builder.addPicture(Offset.zero, textPicture);
const List<Color> colors = <Color>[
Color(0xFF000000),
Color(0xFFFF3C38),
Color(0xFFFF8C42),
Color(0xFFFFF275),
Color(0xFF6699CC),
Color(0xFF656D78),
];
const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
const Rect shaderBounds = Rect.fromLTWH(180, 10, 140, 140);
final EngineGradient shader = GradientLinear(
Offset(200 - shaderBounds.left, 30 - shaderBounds.top),
Offset(320 - shaderBounds.left, 150 - shaderBounds.top),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
builder.pushShaderMask(shader, shaderBounds, blendMode);
final Picture textPicture2 = _drawTestPictureWithText(region, 180, 10);
builder.addPicture(Offset.zero, textPicture2);
builder.pop();
sceneHost.append(builder.build().webOnlyRootElement!);
}

View File

@@ -1,136 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
import '../common/test_initialization.dart';
const Color _kShadowColor = Color.fromARGB(255, 0, 0, 0);
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const Rect region = Rect.fromLTWH(0, 0, 550, 300);
late SurfaceSceneBuilder builder;
setUpUnitTests(emulateTesterEnvironment: false, setUpTestViewDimensions: false);
setUp(() {
builder = SurfaceSceneBuilder();
});
void paintShapeOutline() {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
canvas.drawRect(
const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0),
SurfacePaint()
..color = const Color.fromARGB(255, 0, 0, 255)
..style = PaintingStyle.stroke
..strokeWidth = 1.0,
);
builder.addPicture(Offset.zero, recorder.endRecording());
}
void paintShadowBounds(SurfacePath path, double elevation) {
final Rect shadowBounds = computePenumbraBounds(path.getBounds(), elevation);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
canvas.drawRect(
shadowBounds,
SurfacePaint()
..color = const Color.fromARGB(255, 0, 255, 0)
..style = PaintingStyle.stroke
..strokeWidth = 1.0,
);
builder.addPicture(Offset.zero, recorder.endRecording());
}
void paintBitmapCanvasShadow(double elevation, Offset offset, bool transparentOccluder) {
final SurfacePath path = SurfacePath()..addRect(const Rect.fromLTRB(0, 0, 20, 20));
builder.pushOffset(offset.dx, offset.dy);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
canvas.debugEnforceArbitraryPaint(); // make sure DOM canvas doesn't take over
canvas.drawShadow(path, _kShadowColor, elevation, transparentOccluder);
builder.addPicture(Offset.zero, recorder.endRecording());
paintShapeOutline();
paintShadowBounds(path, elevation);
builder.pop(); // offset
}
void paintBitmapCanvasComplexPathShadow(double elevation, Offset offset) {
final SurfacePath path =
SurfacePath()
..moveTo(10, 0)
..lineTo(20, 10)
..lineTo(10, 20)
..lineTo(0, 10)
..close();
builder.pushOffset(offset.dx, offset.dy);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
canvas.debugEnforceArbitraryPaint(); // make sure DOM canvas doesn't take over
canvas.drawShadow(path, _kShadowColor, elevation, false);
canvas.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color.fromARGB(255, 0, 0, 255),
);
builder.addPicture(Offset.zero, recorder.endRecording());
paintShadowBounds(path, elevation);
builder.pop(); // offset
}
test('renders shadows correctly', () async {
// Physical shape clips. We want to see that clipping in the screenshot.
debugShowClipLayers = false;
builder.pushOffset(10, 20);
for (int i = 0; i < 10; i++) {
paintBitmapCanvasShadow(i.toDouble(), Offset(50.0 * i, 60), false);
}
for (int i = 0; i < 10; i++) {
paintBitmapCanvasShadow(i.toDouble(), Offset(50.0 * i, 120), true);
}
for (int i = 0; i < 10; i++) {
paintBitmapCanvasComplexPathShadow(i.toDouble(), Offset(50.0 * i, 180));
}
builder.pop();
final DomElement sceneElement = builder.build().webOnlyRootElement!;
domDocument.body!.append(sceneElement);
await matchGoldenFile('shadows.png', region: region);
}, testOn: 'chrome');
/// For dart testing having `no tests ran` in a file is considered an error
/// and result in exit code 1.
/// See: https://github.com/dart-lang/test/pull/1173
///
/// Since screenshot tests run one by one and exit code is checked immediately
/// after that a test file that only runs in chrome will break the other
/// browsers. This method is added as a bandaid solution.
test('dummy tests to pass on other browsers', () async {
expect(2 + 2, 4);
});
}

View File

@@ -1,90 +0,0 @@
// 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:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
final Float32List points = Float32List(PathIterator.kMaxBufferSize);
group('PathIterator', () {
test('Should return done verb for empty path', () {
final SurfacePath path = SurfacePath();
final PathIterator iter = PathIterator(path.pathRef, false);
expect(iter.peek(), SPath.kDoneVerb);
expect(iter.next(points), SPath.kDoneVerb);
});
test('Should return done when moveTo is last instruction', () {
final SurfacePath path = SurfacePath();
path.moveTo(10, 10);
final PathIterator iter = PathIterator(path.pathRef, false);
expect(iter.peek(), SPath.kMoveVerb);
expect(iter.next(points), SPath.kDoneVerb);
});
test('Should return lineTo', () {
final SurfacePath path = SurfacePath();
path.moveTo(10, 10);
path.lineTo(20, 20);
final PathIterator iter = PathIterator(path.pathRef, false);
expect(iter.peek(), SPath.kMoveVerb);
expect(iter.next(points), SPath.kMoveVerb);
expect(points[0], 10);
expect(iter.next(points), SPath.kLineVerb);
expect(points[2], 20);
expect(iter.next(points), SPath.kDoneVerb);
});
test('Should return extra lineTo if iteration is closed', () {
final SurfacePath path = SurfacePath();
path.moveTo(10, 10);
path.lineTo(20, 20);
final PathIterator iter = PathIterator(path.pathRef, true);
expect(iter.peek(), SPath.kMoveVerb);
expect(iter.next(points), SPath.kMoveVerb);
expect(points[0], 10);
expect(iter.next(points), SPath.kLineVerb);
expect(points[2], 20);
expect(iter.next(points), SPath.kLineVerb);
expect(points[2], 10);
expect(iter.next(points), SPath.kCloseVerb);
expect(iter.next(points), SPath.kDoneVerb);
});
test('Should not return extra lineTo if last point is starting point', () {
final SurfacePath path = SurfacePath();
path.moveTo(10, 10);
path.lineTo(20, 20);
path.lineTo(10, 10);
final PathIterator iter = PathIterator(path.pathRef, true);
expect(iter.peek(), SPath.kMoveVerb);
expect(iter.next(points), SPath.kMoveVerb);
expect(points[0], 10);
expect(iter.next(points), SPath.kLineVerb);
expect(points[2], 20);
expect(iter.next(points), SPath.kLineVerb);
expect(points[2], 10);
expect(iter.next(points), SPath.kCloseVerb);
expect(iter.next(points), SPath.kDoneVerb);
});
test('peek should return lineTo if iteration is closed', () {
final SurfacePath path = SurfacePath();
path.moveTo(10, 10);
path.lineTo(20, 20);
final PathIterator iter = PathIterator(path.pathRef, true);
expect(iter.next(points), SPath.kMoveVerb);
expect(iter.next(points), SPath.kLineVerb);
expect(iter.peek(), SPath.kLineVerb);
});
});
}

View File

@@ -1,473 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide window;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
/// Test winding and convexity of a path.
void testMain() {
group('Convexity', () {
test('Empty path should be convex', () {
final SurfacePath path = SurfacePath();
expect(path.isConvex, isTrue);
});
test('Circle should be convex', () {
final SurfacePath path = SurfacePath();
path.addOval(const Rect.fromLTRB(0, 0, 20, 20));
expect(path.isConvex, isTrue);
// 2nd circle.
path.addOval(const Rect.fromLTRB(0, 0, 20, 20));
expect(path.isConvex, isFalse);
});
test('addRect should be convex', () {
SurfacePath path = SurfacePath();
path.addRect(const Rect.fromLTRB(0, 0, 20, 20));
expect(path.isConvex, isTrue);
path = SurfacePath();
path.addRectWithDirection(const Rect.fromLTRB(0, 0, 20, 20), SPathDirection.kCW, 0);
expect(path.isConvex, isTrue);
path = SurfacePath();
path.addRectWithDirection(const Rect.fromLTRB(0, 0, 20, 20), SPathDirection.kCCW, 0);
expect(path.isConvex, isTrue);
});
test('Quad should be convex', () {
final SurfacePath path = SurfacePath();
path.quadraticBezierTo(100, 100, 50, 50);
expect(path.isConvex, isTrue);
});
test('moveto/lineto convexity', () {
final List<LineTestCase> testCases = <LineTestCase>[
LineTestCase('', SPathConvexityType.kConvex),
LineTestCase('0 0', SPathConvexityType.kConvex),
LineTestCase('0 0 10 10', SPathConvexityType.kConvex),
LineTestCase('0 0 10 10 20 20 0 0 10 10', SPathConvexityType.kConcave),
LineTestCase('0 0 10 10 10 20', SPathConvexityType.kConvex),
LineTestCase('0 0 10 10 10 0', SPathConvexityType.kConvex),
LineTestCase('0 0 10 10 10 0 0 10', SPathConvexityType.kConcave),
LineTestCase('0 0 10 0 0 10 -10 -10', SPathConvexityType.kConcave),
];
for (final LineTestCase testCase in testCases) {
final SurfacePath path = SurfacePath();
setFromString(path, testCase.pathContent);
expect(path.convexityType, testCase.convexity);
}
});
test('Convexity of path with infinite points should return unknown', () {
const List<Offset> nonFinitePts = <Offset>[
Offset(double.infinity, 0),
Offset(0, double.infinity),
Offset.infinite,
Offset(double.negativeInfinity, 0),
Offset(0, double.negativeInfinity),
Offset(double.negativeInfinity, double.negativeInfinity),
Offset(double.negativeInfinity, double.infinity),
Offset(double.infinity, double.negativeInfinity),
Offset(double.nan, 0),
Offset(0, double.nan),
Offset(double.nan, double.nan),
];
final int nonFinitePointsCount = nonFinitePts.length;
const List<Offset> axisAlignedPts = <Offset>[
Offset(kScalarMax, 0),
Offset(0, kScalarMax),
Offset(kScalarMin, 0),
Offset(0, kScalarMin),
];
final int axisAlignedPointsCount = axisAlignedPts.length;
final SurfacePath path = SurfacePath();
for (int index = 0; index < (13 * nonFinitePointsCount * axisAlignedPointsCount); index++) {
final int i = index % nonFinitePointsCount;
final int f = index % axisAlignedPointsCount;
final int g = (f + 1) % axisAlignedPointsCount;
path.reset();
switch (index % 13) {
case 0:
path.lineTo(nonFinitePts[i].dx, nonFinitePts[i].dy);
case 1:
path.quadraticBezierTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
);
case 2:
path.quadraticBezierTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 3:
path.quadraticBezierTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
);
case 4:
path.cubicTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 5:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 6:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
);
case 7:
path.cubicTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 8:
path.cubicTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
);
case 9:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
);
case 10:
path.cubicTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
nonFinitePts[i].dx,
nonFinitePts[i].dy,
);
case 11:
path.cubicTo(
nonFinitePts[i].dx,
nonFinitePts[i].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
);
case 12:
path.moveTo(nonFinitePts[i].dx, nonFinitePts[i].dy);
}
expect(path.convexityType, SPathConvexityType.kUnknown);
}
for (int index = 0; index < (11 * axisAlignedPointsCount); ++index) {
final int f = index % axisAlignedPointsCount;
final int g = (f + 1) % axisAlignedPointsCount;
path.reset();
final int curveSelect = index % 11;
switch (curveSelect) {
case 0:
path.moveTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy);
case 1:
path.lineTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy);
case 2:
path.quadraticBezierTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 3:
path.quadraticBezierTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
);
case 4:
path.quadraticBezierTo(
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 5:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 6:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
);
case 7:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 8:
path.cubicTo(
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
);
case 9:
path.cubicTo(
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
);
case 10:
path.cubicTo(
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
axisAlignedPts[f].dx,
axisAlignedPts[f].dy,
axisAlignedPts[g].dx,
axisAlignedPts[g].dy,
);
}
if (curveSelect != 7 && curveSelect != 10) {
final int result = path.convexityType;
expect(result, SPathConvexityType.kConvex);
} else {
// we make a copy so that we don't cache the result on the passed
// in path.
final SurfacePath path2 = SurfacePath.from(path);
final int c = path2.convexityType;
assert(SPathConvexityType.kUnknown == c || SPathConvexityType.kConcave == c);
}
}
});
test('Concave lines path', () {
final SurfacePath path = SurfacePath();
path.moveTo(-0.284071773, -0.0622361786);
path.lineTo(-0.284072, -0.0622351);
path.lineTo(-0.28407, -0.0622307);
path.lineTo(-0.284067, -0.0622182);
path.lineTo(-0.284084, -0.0622269);
path.lineTo(-0.284072, -0.0622362);
path.close();
expect(path.convexityType, SPathConvexityType.kConcave);
});
test('Single moveTo origin', () {
final SurfacePath path = SurfacePath();
path.moveTo(0, 0);
expect(path.convexityType, SPathConvexityType.kConvex);
});
test('Single diagonal line', () {
final SurfacePath path = SurfacePath();
path.moveTo(12, 20);
path.lineTo(-12, -20);
expect(path.convexityType, SPathConvexityType.kConvex);
});
test('TriLeft', () {
final SurfacePath path = SurfacePath();
path.moveTo(0, 0);
path.lineTo(1, 0);
path.lineTo(1, 1);
path.close();
expect(path.convexityType, SPathConvexityType.kConvex);
});
test('TriRight', () {
final SurfacePath path = SurfacePath();
path.moveTo(0, 0);
path.lineTo(-1, 0);
path.lineTo(1, 1);
path.close();
expect(path.convexityType, SPathConvexityType.kConvex);
});
test('square', () {
final SurfacePath path = SurfacePath();
path.moveTo(0, 0);
path.lineTo(1, 0);
path.lineTo(1, 1);
path.lineTo(0, 1);
path.close();
expect(path.convexityType, SPathConvexityType.kConvex);
});
test('redundant square', () {
final SurfacePath redundantSquare = SurfacePath();
redundantSquare.moveTo(0, 0);
redundantSquare.lineTo(0, 0);
redundantSquare.lineTo(0, 0);
redundantSquare.lineTo(1, 0);
redundantSquare.lineTo(1, 0);
redundantSquare.lineTo(1, 0);
redundantSquare.lineTo(1, 1);
redundantSquare.lineTo(1, 1);
redundantSquare.lineTo(1, 1);
redundantSquare.lineTo(0, 1);
redundantSquare.lineTo(0, 1);
redundantSquare.lineTo(0, 1);
redundantSquare.close();
expect(redundantSquare.convexityType, SPathConvexityType.kConvex);
});
test('bowtie', () {
final SurfacePath bowTie = SurfacePath();
bowTie.moveTo(0, 0);
bowTie.lineTo(0, 0);
bowTie.lineTo(0, 0);
bowTie.lineTo(1, 1);
bowTie.lineTo(1, 1);
bowTie.lineTo(1, 1);
bowTie.lineTo(1, 0);
bowTie.lineTo(1, 0);
bowTie.lineTo(1, 0);
bowTie.lineTo(0, 1);
bowTie.lineTo(0, 1);
bowTie.lineTo(0, 1);
bowTie.close();
expect(bowTie.convexityType, SPathConvexityType.kConcave);
});
test('sprial', () {
final SurfacePath spiral = SurfacePath();
spiral.moveTo(0, 0);
spiral.lineTo(100, 0);
spiral.lineTo(100, 100);
spiral.lineTo(0, 100);
spiral.lineTo(0, 50);
spiral.lineTo(50, 50);
spiral.lineTo(50, 75);
spiral.close();
expect(spiral.convexityType, SPathConvexityType.kConcave);
});
test('dent', () {
final SurfacePath dent = SurfacePath();
dent.moveTo(0, 0);
dent.lineTo(100, 100);
dent.lineTo(0, 100);
dent.lineTo(-50, 200);
dent.lineTo(-200, 100);
dent.close();
expect(dent.convexityType, SPathConvexityType.kConcave);
});
test('degenerate segments1', () {
final SurfacePath strokedSin = SurfacePath();
for (int i = 0; i < 2000; i++) {
final double x = i.toDouble() / 2.0;
final double y = 500 - (x + math.sin(x / 100) * 40) / 3;
if (0 == i) {
strokedSin.moveTo(x, y);
} else {
strokedSin.lineTo(x, y);
}
}
expect(strokedSin.convexityType, SPathConvexityType.kConcave);
});
/// Regression test for https://github.com/flutter/flutter/issues/66560.
test('Quadratic', () {
final SurfacePath path = SurfacePath();
path.moveTo(100.0, 0.0);
path.quadraticBezierTo(200.0, 0.0, 200.0, 100.0);
path.quadraticBezierTo(200.0, 200.0, 100.0, 200.0);
path.quadraticBezierTo(0.0, 200.0, 0.0, 100.0);
path.quadraticBezierTo(0.0, 0.0, 100.0, 0.0);
path.close();
expect(path.contains(const Offset(100, 20)), isTrue);
expect(path.contains(const Offset(100, 120)), isTrue);
expect(path.contains(const Offset(100, -10)), isFalse);
});
});
}
class LineTestCase {
LineTestCase(this.pathContent, this.convexity);
final String pathContent;
final int convexity;
}
/// Parses a string of the format "mx my lx1 ly1 lx2 ly2..." into a path
/// with moveTo/lineTo instructions for points.
void setFromString(SurfacePath path, String value) {
bool first = true;
final List<String> points = value.split(' ');
if (points.length < 2) {
return;
}
for (int i = 0; i < points.length; i += 2) {
if (first) {
path.moveTo(double.parse(points[i]), double.parse(points[i + 1]));
first = false;
} else {
path.lineTo(double.parse(points[i]), double.parse(points[i + 1]));
}
}
}
// Scalar max is based on 32 bit float since [PathRef] stores values in
// Float32List.
const double kScalarMax = 3.402823466e+38;
const double kScalarMin = -kScalarMax;

View File

@@ -1,93 +0,0 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../../common/matchers.dart';
import '../../common/test_initialization.dart';
const MethodCodec codec = StandardMethodCodec();
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
await bootstrapAndRunApp(withImplicitView: true);
late PersistedPlatformView view;
group('PersistedPlatformView', () {
setUp(() async {
ui_web.platformViewRegistry.registerViewFactory(
'test-0',
(int viewId) => createDomHTMLDivElement(),
);
ui_web.platformViewRegistry.registerViewFactory(
'test-1',
(int viewId) => createDomHTMLDivElement(),
);
// Ensure the views are created...
await Future.wait(<Future<void>>[
_createPlatformView(0, 'test-0'),
_createPlatformView(1, 'test-1'),
]);
view = PersistedPlatformView(0, 0, 0, 100, 100)..build();
});
group('update', () {
test('throws assertion error if called with different viewIds', () {
final PersistedPlatformView differentView = PersistedPlatformView(1, 1, 1, 100, 100)
..build();
expect(() {
view.update(differentView);
}, throwsAssertionError);
});
});
group('canUpdateAsMatch', () {
test('returns true when viewId is the same', () {
final PersistedPlatformView sameView = PersistedPlatformView(0, 1, 1, 100, 100)..build();
expect(view.canUpdateAsMatch(sameView), isTrue);
});
test('returns false when viewId is different', () {
final PersistedPlatformView differentView = PersistedPlatformView(1, 1, 1, 100, 100)
..build();
expect(view.canUpdateAsMatch(differentView), isFalse);
});
test('returns false when other view is not a PlatformView', () {
final PersistedOpacity anyView = PersistedOpacity(null, 1, ui.Offset.zero)..build();
expect(view.canUpdateAsMatch(anyView), isFalse);
});
});
group('createElement', () {
test('creates slot element that can receive pointer events', () {
final DomElement element = view.createElement();
expect(element.tagName, equalsIgnoringCase('flt-platform-view-slot'));
expect(element.style.pointerEvents, 'auto');
});
});
});
}
// Sends a platform message to create a Platform View with the given id and viewType.
Future<void> _createPlatformView(int id, String viewType) {
final Completer<void> completer = Completer<void>();
ui.PlatformDispatcher.instance.sendPlatformMessage(
'flutter/platform_views',
codec.encodeMethodCall(MethodCall('create', <String, dynamic>{'id': id, 'viewType': viewType})),
(dynamic _) => completer.complete(),
);
return completer.future;
}

View File

@@ -1,961 +0,0 @@
// 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.
@TestOn('chrome || firefox')
library;
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../../common/matchers.dart';
import '../../common/rendering.dart';
import '../../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpAll(() async {
await bootstrapAndRunApp(withImplicitView: true);
setUpRenderingForTests();
});
group('SceneBuilder', () {
test('pushOffset implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
return sceneBuilder.pushOffset(10, 20, oldLayer: oldLayer as ui.OffsetEngineLayer?);
},
() {
return '''<flt-scene><flt-offset></flt-offset></flt-scene>''';
},
);
});
test('pushTransform implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
return sceneBuilder.pushTransform(
(Matrix4.identity()..scale(EngineFlutterDisplay.instance.browserDevicePixelRatio))
.toFloat64(),
);
},
() {
return '''<flt-scene><flt-transform></flt-transform></flt-scene>''';
},
);
});
test('pushClipRect implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
return sceneBuilder.pushClipRect(
const ui.Rect.fromLTRB(10, 20, 30, 40),
oldLayer: oldLayer as ui.ClipRectEngineLayer?,
);
},
() {
return '''
<flt-scene>
<flt-clip><flt-clip-interior></flt-clip-interior></flt-clip>
</flt-scene>
''';
},
);
});
test('pushClipRRect implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
return sceneBuilder.pushClipRRect(
ui.RRect.fromLTRBR(10, 20, 30, 40, const ui.Radius.circular(3)),
oldLayer: oldLayer as ui.ClipRRectEngineLayer?,
clipBehavior: ui.Clip.none,
);
},
() {
return '''
<flt-scene>
<flt-clip clip-type="rrect">
<flt-clip-interior></flt-clip-interior>
</flt-clip>
</flt-scene>
''';
},
);
});
test('pushClipPath implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
final ui.Path path = ui.Path()..addRect(const ui.Rect.fromLTRB(10, 20, 30, 40));
return sceneBuilder.pushClipPath(path, oldLayer: oldLayer as ui.ClipPathEngineLayer?);
},
() {
return '''
<flt-scene>
<flt-clippath>
<svg><defs><clipPath><path></path></clipPath></defs></svg>
</flt-clippath>
</flt-scene>
''';
},
);
});
test('pushOpacity implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
return sceneBuilder.pushOpacity(10, oldLayer: oldLayer as ui.OpacityEngineLayer?);
},
() {
return '''<flt-scene><flt-opacity></flt-opacity></flt-scene>''';
},
);
});
test('pushBackdropFilter implements surface lifecycle', () {
testLayerLifeCycle(
(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) {
return sceneBuilder.pushBackdropFilter(
ui.ImageFilter.blur(sigmaX: 1.0, sigmaY: 1.0),
oldLayer: oldLayer as ui.BackdropFilterEngineLayer?,
);
},
() {
return '''
<flt-scene>
<flt-backdrop>
<flt-backdrop-filter></flt-backdrop-filter>
<flt-backdrop-interior></flt-backdrop-interior>
</flt-backdrop>
</flt-scene>''';
},
);
});
});
group('parent child lifecycle', () {
test('build, retain, update, and applyPaint are called the right number of times', () {
final PersistedScene scene1 = PersistedScene(null);
final PersistedClipRect clip1 = PersistedClipRect(
null,
const ui.Rect.fromLTRB(10, 10, 20, 20),
ui.Clip.antiAlias,
);
final PersistedOpacity opacity = PersistedOpacity(null, 100, ui.Offset.zero);
final MockPersistedPicture picture = MockPersistedPicture();
scene1.appendChild(clip1);
clip1.appendChild(opacity);
opacity.appendChild(picture);
expect(picture.retainCount, 0);
expect(picture.buildCount, 0);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 0);
scene1.preroll(PrerollSurfaceContext());
scene1.build();
commitScene(scene1);
expect(picture.retainCount, 0);
expect(picture.buildCount, 1);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 1);
// The second scene graph retains the opacity, but not the clip. However,
// because the clip didn't change no repaints should happen.
final PersistedScene scene2 = PersistedScene(scene1);
final PersistedClipRect clip2 = PersistedClipRect(
clip1,
const ui.Rect.fromLTRB(10, 10, 20, 20),
ui.Clip.antiAlias,
);
clip1.state = PersistedSurfaceState.pendingUpdate;
scene2.appendChild(clip2);
opacity.state = PersistedSurfaceState.pendingRetention;
clip2.appendChild(opacity);
scene2.preroll(PrerollSurfaceContext());
scene2.update(scene1);
commitScene(scene2);
expect(picture.retainCount, 1);
expect(picture.buildCount, 1);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 1);
// The third scene graph retains the opacity, and produces a new clip.
// This should cause the picture to repaint despite being retained.
final PersistedScene scene3 = PersistedScene(scene2);
final PersistedClipRect clip3 = PersistedClipRect(
clip2,
const ui.Rect.fromLTRB(10, 10, 50, 50),
ui.Clip.antiAlias,
);
clip2.state = PersistedSurfaceState.pendingUpdate;
scene3.appendChild(clip3);
opacity.state = PersistedSurfaceState.pendingRetention;
clip3.appendChild(opacity);
scene3.preroll(PrerollSurfaceContext());
scene3.update(scene2);
commitScene(scene3);
expect(picture.retainCount, 2);
expect(picture.buildCount, 1);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 2);
});
});
group('Compositing order', () {
// Regression test for https://github.com/flutter/flutter/issues/55058
//
// When BitmapCanvas uses multiple elements to paint, the very first
// canvas needs to have a -1 zIndex so it can preserve compositing order.
test('Canvas element should retain -1 zIndex after update', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Picture picture1 = _drawPicture();
final ui.ClipRectEngineLayer oldLayer = builder.pushClipRect(
const ui.Rect.fromLTRB(10, 10, 300, 300),
);
builder.addPicture(ui.Offset.zero, picture1);
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelector('canvas')!.style.zIndex, '-1');
// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushClipRect(const ui.Rect.fromLTRB(5, 10, 300, 300), oldLayer: oldLayer);
final ui.Picture picture2 = _drawPicture();
builder2.addPicture(ui.Offset.zero, picture2);
builder2.pop();
final DomElement contentAfterReuse = builder2.build().webOnlyRootElement!;
expect(contentAfterReuse.querySelector('canvas')!.style.zIndex, '-1');
});
test('Multiple canvas elements should retain zIndex after update', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Picture picture1 = _drawPathImagePath();
final ui.ClipRectEngineLayer oldLayer = builder.pushClipRect(
const ui.Rect.fromLTRB(10, 10, 300, 300),
);
builder.addPicture(ui.Offset.zero, picture1);
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
domDocument.body!.append(content);
expect(content.querySelector('canvas')!.style.zIndex, '-1');
// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushClipRect(const ui.Rect.fromLTRB(5, 10, 300, 300), oldLayer: oldLayer);
final ui.Picture picture2 = _drawPathImagePath();
builder2.addPicture(ui.Offset.zero, picture2);
builder2.pop();
final DomElement contentAfterReuse = builder2.build().webOnlyRootElement!;
final List<DomCanvasElement> list =
contentAfterReuse.querySelectorAll('canvas').cast<DomCanvasElement>().toList();
expect(list[0].style.zIndex, '-1');
expect(list[1].style.zIndex, '');
});
});
/// Verify elementCache is passed during update to reuse existing
/// image elements.
test('Should retain same image element', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Picture picture1 = _drawPathImagePath();
final ui.ClipRectEngineLayer oldLayer = builder.pushClipRect(
const ui.Rect.fromLTRB(10, 10, 300, 300),
);
builder.addPicture(ui.Offset.zero, picture1);
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
domDocument.body!.append(content);
List<DomHTMLImageElement> list =
content.querySelectorAll('img').cast<DomHTMLImageElement>().toList();
for (final DomHTMLImageElement image in list) {
image.alt = 'marked';
}
// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushClipRect(const ui.Rect.fromLTRB(5, 10, 300, 300), oldLayer: oldLayer);
final ui.Picture picture2 = _drawPathImagePath();
builder2.addPicture(ui.Offset.zero, picture2);
builder2.pop();
final DomElement contentAfterReuse = builder2.build().webOnlyRootElement!;
list = contentAfterReuse.querySelectorAll('img').cast<DomHTMLImageElement>().toList();
for (final DomHTMLImageElement image in list) {
expect(image.alt, 'marked');
}
expect(list.length, 1);
});
PersistedPicture? findPictureSurfaceChild(PersistedContainerSurface parent) {
PersistedPicture? pictureSurface;
parent.visitChildren((PersistedSurface child) {
pictureSurface = child as PersistedPicture;
});
return pictureSurface;
}
test('skips painting picture when picture fully clipped out', () async {
final ui.Picture picture = _drawPicture();
// Picture not clipped out, so we should see a `<flt-canvas>`
{
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 0);
builder.addPicture(ui.Offset.zero, picture);
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isNotEmpty);
}
// Picture fully clipped out, so we should not see a `<flt-canvas>`
{
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 0);
final PersistedContainerSurface clip =
builder.pushClipRect(const ui.Rect.fromLTRB(1000, 1000, 2000, 2000))
as PersistedContainerSurface;
builder.addPicture(ui.Offset.zero, picture);
builder.pop();
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isEmpty);
expect(findPictureSurfaceChild(clip)!.canvas, isNull);
}
});
test('does not skip painting picture when picture is '
'inside transform with offset', () async {
final ui.Picture picture = _drawPicture();
// Picture should not be clipped out since transform will offset it to 500,500
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 0);
builder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 1000, 1000)) as PersistedContainerSurface;
builder.pushTransform((Matrix4.identity()..scale(0.5, 0.5)).toFloat64());
builder.addPicture(const ui.Offset(1000, 1000), picture);
builder.pop();
builder.pop();
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isNotEmpty);
});
test('does not skip painting picture when picture is '
'inside transform', () async {
final ui.Picture picture = _drawPicture();
// Picture should not be clipped out since transform will offset it to 500,500
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 0);
builder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 1000, 1000)) as PersistedContainerSurface;
builder.pushTransform((Matrix4.identity()..scale(0.5, 0.5)).toFloat64());
builder.pushOffset(1000, 1000);
builder.addPicture(ui.Offset.zero, picture);
builder.pop();
builder.pop();
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isNotEmpty);
});
test('skips painting picture when picture fully clipped out with'
' transform and offset', () async {
final ui.Picture picture = _drawPicture();
// Picture should be clipped out since transform will offset it to 500,500
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(50, 50);
builder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 1000, 1000)) as PersistedContainerSurface;
builder.pushTransform((Matrix4.identity()..scale(2, 2)).toFloat64());
builder.pushOffset(500, 500);
builder.addPicture(ui.Offset.zero, picture);
builder.pop();
builder.pop();
builder.pop();
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isEmpty);
});
test('releases old canvas when picture is fully clipped out after addRetained', () async {
final ui.Picture picture = _drawPicture();
// Frame 1: picture visible
final SurfaceSceneBuilder builder1 = SurfaceSceneBuilder();
final PersistedOffset offset1 = builder1.pushOffset(0, 0) as PersistedOffset;
builder1.addPicture(ui.Offset.zero, picture);
builder1.pop();
final DomElement content1 = builder1.build().webOnlyRootElement!;
expect(content1.querySelectorAll('flt-picture').single.children, isNotEmpty);
expect(findPictureSurfaceChild(offset1)!.canvas, isNotNull);
// Frame 2: picture is clipped out after an update
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
final PersistedOffset offset2 =
builder2.pushOffset(-10000, -10000, oldLayer: offset1) as PersistedOffset;
builder2.addPicture(ui.Offset.zero, picture);
builder2.pop();
final DomElement content = builder2.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isEmpty);
expect(findPictureSurfaceChild(offset2)!.canvas, isNull);
});
test('releases old canvas when picture is fully clipped out after addRetained', () async {
final ui.Picture picture = _drawPicture();
// Frame 1: picture visible
final SurfaceSceneBuilder builder1 = SurfaceSceneBuilder();
final PersistedOffset offset1 = builder1.pushOffset(0, 0) as PersistedOffset;
final PersistedOffset subOffset1 = builder1.pushOffset(0, 0) as PersistedOffset;
builder1.addPicture(ui.Offset.zero, picture);
builder1.pop();
builder1.pop();
final DomElement content1 = builder1.build().webOnlyRootElement!;
expect(content1.querySelectorAll('flt-picture').single.children, isNotEmpty);
expect(findPictureSurfaceChild(subOffset1)!.canvas, isNotNull);
// Frame 2: picture is clipped out after addRetained
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushOffset(-10000, -10000, oldLayer: offset1);
// Even though the child offset is added as retained, the parent
// is updated with a value that causes the picture to move out of
// the clipped area. We should see the canvas being released.
builder2.addRetained(subOffset1);
builder2.pop();
final DomElement content = builder2.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-picture').single.children, isEmpty);
expect(findPictureSurfaceChild(subOffset1)!.canvas, isNull);
});
test('auto-pops pushed layers', () async {
final ui.Picture picture = _drawPicture();
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 0);
builder.pushOffset(0, 0);
builder.pushOffset(0, 0);
builder.pushOffset(0, 0);
builder.pushOffset(0, 0);
builder.addPicture(ui.Offset.zero, picture);
// Intentionally pop fewer layers than we pushed
builder.pop();
builder.pop();
builder.pop();
// Expect as many layers as we pushed (not popped).
final DomElement content = builder.build().webOnlyRootElement!;
expect(content.querySelectorAll('flt-offset'), hasLength(5));
});
test('updates child lists efficiently', () async {
// Pushes a single child that renders one character.
//
// If the character is a number, pushes an offset layer. Otherwise, pushes
// an offset layer. Test cases use this to control how layers are reused.
// Layers of the same type can be reused even if they are not explicitly
// updated. Conversely, layers of different types are never reused.
ui.EngineLayer pushChild(SurfaceSceneBuilder builder, String char, {ui.EngineLayer? oldLayer}) {
// Numbers use opacity layers, letters use offset layers. This is used to
// control DOM reuse. Layers of the same type can reuse DOM nodes from other
// dropped layers.
final bool useOffset = int.tryParse(char) == null;
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(
const ui.Rect.fromLTRB(0, 0, 400, 400),
);
final ui.Paragraph paragraph =
(ui.ParagraphBuilder(ui.ParagraphStyle())
..pushStyle(ui.TextStyle(decoration: ui.TextDecoration.lineThrough))
..addText(char))
.build();
paragraph.layout(const ui.ParagraphConstraints(width: 1000));
canvas.drawParagraph(paragraph, ui.Offset.zero);
final ui.EngineLayer newLayer =
useOffset
? builder.pushOffset(
0,
0,
oldLayer: oldLayer == null ? null : oldLayer as ui.OffsetEngineLayer,
)
: builder.pushOpacity(
100,
oldLayer: oldLayer == null ? null : oldLayer as ui.OpacityEngineLayer,
);
builder.addPicture(ui.Offset.zero, recorder.endRecording());
builder.pop();
return newLayer;
}
// Maps letters to layers used to render them in the last frame, used to
// supply `oldLayer` to guarantee update.
final Map<String, ui.EngineLayer> renderedLayers = <String, ui.EngineLayer>{};
// Pump an empty scene to reset it, otherwise the first frame will attempt
// to diff left-overs from a previous test, which results in unpredictable
// DOM mutations.
await renderScene(SurfaceSceneBuilder().build());
// Renders a `string` by breaking it up into individual characters and
// rendering each character into its own layer.
Future<void> testCase(
String string,
String description, {
int deletions = 0,
int additions = 0,
int moves = 0,
}) {
final Set<DomNode> actualDeletions = <DomNode>{};
final Set<DomNode> actualAdditions = <DomNode>{};
// Watches DOM mutations and counts deletions and additions to the child
// list of the `<flt-scene>` element.
final DomMutationObserver observer = createDomMutationObserver((
JSArray<JSAny?> mutations,
_,
) {
for (final DomMutationRecord record in mutations.toDart.cast<DomMutationRecord>()) {
actualDeletions.addAll(record.removedNodes!);
actualAdditions.addAll(record.addedNodes!);
}
});
observer.observe(SurfaceSceneBuilder.debugLastFrameScene!.rootElement!, childList: true);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
for (int i = 0; i < string.length; i++) {
final String char = string[i];
renderedLayers[char] = pushChild(builder, char, oldLayer: renderedLayers[char]);
}
final SurfaceScene scene = builder.build();
final List<DomElement> pTags =
scene.webOnlyRootElement!.querySelectorAll('flt-paragraph').toList();
expect(pTags, hasLength(string.length));
expect(
scene.webOnlyRootElement!
.querySelectorAll('flt-paragraph')
.map((DomElement p) => p.innerText)
.join(),
string,
);
renderedLayers.removeWhere((String key, ui.EngineLayer value) => !string.contains(key));
// Inject a zero-duration timer to allow mutation observers to receive notification.
return Future<void>.delayed(Duration.zero).then((_) {
observer.disconnect();
// Nodes that are removed then added are classified as "moves".
final int actualMoves = actualAdditions.intersection(actualDeletions).length;
// Compare all at once instead of one by one because when it fails, it's
// much more useful to see all numbers, not just the one that failed to
// match.
expect(
<String, int>{
'additions': actualAdditions.length - actualMoves,
'deletions': actualDeletions.length - actualMoves,
'moves': actualMoves,
},
<String, int>{'additions': additions, 'deletions': deletions, 'moves': moves},
);
});
}
// Adding
await testCase('', 'noop');
await testCase('', 'noop');
await testCase('be', 'zero-to-many', additions: 2);
await testCase('bcde', 'insert in the middle', additions: 2);
await testCase('abcde', 'prepend', additions: 1);
await testCase('abcdef', 'append', additions: 1);
// Moving
await testCase('fbcdea', 'swap at ends', moves: 2);
await testCase('fecdba', 'swap in the middle', moves: 2);
await testCase('fedcba', 'swap adjacent in one move', moves: 1);
await testCase('fedcba', 'non-empty noop');
await testCase('afedcb', 'shift right by 1', moves: 1);
await testCase('fedcba', 'shift left by 1', moves: 1);
await testCase('abcdef', 'reverse', moves: 5);
await testCase('efabcd', 'shift right by 2', moves: 2);
await testCase('abcdef', 'shift left by 2', moves: 2);
// Scrolling without DOM reuse (numbers and letters use different types of layers)
await testCase('9abcde', 'scroll right by 1', additions: 1, deletions: 1);
await testCase('789abc', 'scroll right by 2', additions: 2, deletions: 2);
await testCase('89abcd', 'scroll left by 1', additions: 1, deletions: 1);
await testCase('abcdef', 'scroll left by 2', additions: 2, deletions: 2);
// Scrolling with DOM reuse
await testCase('zabcde', 'scroll right by 1', moves: 1);
await testCase('xyzabc', 'scroll right by 2', moves: 2);
await testCase('yzabcd', 'scroll left by 1', moves: 1);
await testCase('abcdef', 'scroll left by 2', moves: 2);
// Removing
await testCase('bcdef', 'remove as start', deletions: 1);
await testCase('bcde', 'remove as end', deletions: 1);
await testCase('be', 'remove in the middle', deletions: 2);
await testCase('', 'remove all', deletions: 2);
});
test('Canvas should allocate fewer pixels when zoomed out', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Picture picture1 = _drawPicture();
builder.pushClipRect(const ui.Rect.fromLTRB(10, 10, 300, 300));
builder.addPicture(ui.Offset.zero, picture1);
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
final DomCanvasElement canvas = content.querySelector('canvas')! as DomCanvasElement;
final int unscaledWidth = canvas.width!.toInt();
final int unscaledHeight = canvas.height!.toInt();
// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushOffset(0, 0);
builder2.pushTransform(Matrix4.identity().scaled(0.5, 0.5).toFloat64());
builder2.pushClipRect(const ui.Rect.fromLTRB(10, 10, 300, 300));
builder2.addPicture(ui.Offset.zero, picture1);
builder2.pop();
builder2.pop();
builder2.pop();
final DomElement contentAfterScale = builder2.build().webOnlyRootElement!;
final DomCanvasElement canvas2 = contentAfterScale.querySelector('canvas')! as DomCanvasElement;
// Although we are drawing same picture, due to scaling the new canvas
// should have fewer pixels.
expect(canvas2.width! < unscaledWidth, isTrue);
expect(canvas2.height! < unscaledHeight, isTrue);
});
test('Canvas should allocate more pixels when zoomed in', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Picture picture1 = _drawPicture();
builder.pushClipRect(const ui.Rect.fromLTRB(10, 10, 300, 300));
builder.addPicture(ui.Offset.zero, picture1);
builder.pop();
final DomElement content = builder.build().webOnlyRootElement!;
final DomCanvasElement canvas = content.querySelector('canvas')! as DomCanvasElement;
final int unscaledWidth = canvas.width!.toInt();
final int unscaledHeight = canvas.height!.toInt();
// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushOffset(0, 0);
builder2.pushTransform(Matrix4.identity().scaled(2, 2).toFloat64());
builder2.pushClipRect(const ui.Rect.fromLTRB(10, 10, 300, 300));
builder2.addPicture(ui.Offset.zero, picture1);
builder2.pop();
builder2.pop();
builder2.pop();
final DomElement contentAfterScale = builder2.build().webOnlyRootElement!;
final DomCanvasElement canvas2 = contentAfterScale.querySelector('canvas')! as DomCanvasElement;
// Although we are drawing same picture, due to scaling the new canvas
// should have more pixels.
expect(canvas2.width! > unscaledWidth, isTrue);
expect(canvas2.height! > unscaledHeight, isTrue);
});
test('Should recycle canvas once', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final ui.Picture picture1 = _drawPicture();
final ui.ClipRectEngineLayer oldLayer = builder.pushClipRect(
const ui.Rect.fromLTRB(10, 10, 300, 300),
);
builder.addPicture(ui.Offset.zero, picture1);
builder.pop();
builder.build();
// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
final ui.ClipRectEngineLayer oldLayer2 = builder2.pushClipRect(
const ui.Rect.fromLTRB(5, 10, 300, 300),
oldLayer: oldLayer,
);
builder2.addPicture(ui.Offset.zero, _drawEmptyPicture());
builder2.pop();
final DomElement contentAfterReuse = builder2.build().webOnlyRootElement!;
expect(contentAfterReuse, isNotNull);
final SurfaceSceneBuilder builder3 = SurfaceSceneBuilder();
builder3.pushClipRect(const ui.Rect.fromLTRB(25, 10, 300, 300), oldLayer: oldLayer2);
builder3.addPicture(ui.Offset.zero, _drawEmptyPicture());
builder3.pop();
// This build will crash if canvas gets recycled twice.
final DomElement contentAfterReuse2 = builder3.build().webOnlyRootElement!;
expect(contentAfterReuse2, isNotNull);
});
}
typedef TestLayerBuilder =
ui.EngineLayer Function(ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer);
typedef ExpectedHtmlGetter = String Function();
void testLayerLifeCycle(TestLayerBuilder layerBuilder, ExpectedHtmlGetter expectedHtmlGetter) {
// Force scene builder to start from scratch. This guarantees that the first
// scene starts from the "build" phase.
SurfaceSceneBuilder.debugForgetFrameScene();
// Build: builds a brand new layer.
SurfaceSceneBuilder sceneBuilder = SurfaceSceneBuilder();
final ui.EngineLayer layer1 = layerBuilder(sceneBuilder, null);
final Type surfaceType = layer1.runtimeType;
sceneBuilder.pop();
SceneTester tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
PersistedSurface findSurface() {
return enumerateSurfaces().where((PersistedSurface s) => s.runtimeType == surfaceType).single;
}
final PersistedSurface surface1 = findSurface();
final DomElement surfaceElement1 = surface1.rootElement!;
// Retain: reuses a layer as is along with its DOM elements.
sceneBuilder = SurfaceSceneBuilder();
sceneBuilder.addRetained(layer1);
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
final PersistedSurface surface2 = findSurface();
final DomElement surfaceElement2 = surface2.rootElement!;
expect(surface2, same(surface1));
expect(surfaceElement2, same(surfaceElement1));
// Reuse: reuses a layer's DOM elements by matching it.
sceneBuilder = SurfaceSceneBuilder();
final ui.EngineLayer layer3 = layerBuilder(sceneBuilder, layer1);
sceneBuilder.pop();
expect(layer3, isNot(same(layer1)));
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
final PersistedSurface surface3 = findSurface();
expect(surface3, same(layer3));
final DomElement surfaceElement3 = surface3.rootElement!;
expect(surface3, isNot(same(surface2)));
expect(surfaceElement3, isNotNull);
expect(surfaceElement3, same(surfaceElement2));
// Recycle: discards all the layers.
sceneBuilder = SurfaceSceneBuilder();
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml('<flt-scene></flt-scene>');
expect(surface3.rootElement, isNull); // offset3 should be recycled.
// Retain again: the framework should be able to request that a layer is added
// as retained even after it has been recycled. In this case the
// engine would "rehydrate" the layer with new DOM elements.
sceneBuilder = SurfaceSceneBuilder();
sceneBuilder.addRetained(layer3);
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
expect(surface3.rootElement, isNotNull); // offset3 should be rehydrated.
// Make sure we clear retained surface list.
expect(retainedSurfaces, isEmpty);
}
class MockPersistedPicture extends PersistedPicture {
factory MockPersistedPicture() {
final EnginePictureRecorder recorder = EnginePictureRecorder();
// Use the largest cull rect so that layer clips are effective. The tests
// rely on this.
recorder.beginRecording(ui.Rect.largest).drawPaint(SurfacePaint());
return MockPersistedPicture._(recorder.endRecording());
}
MockPersistedPicture._(EnginePicture picture) : super(0, 0, picture, 0);
int retainCount = 0;
int buildCount = 0;
int updateCount = 0;
int applyPaintCount = 0;
final BitmapCanvas _fakeCanvas = BitmapCanvas(
const ui.Rect.fromLTRB(0, 0, 10, 10),
RenderStrategy(),
);
@override
EngineCanvas get canvas {
return _fakeCanvas;
}
@override
double matchForUpdate(PersistedPicture existingSurface) {
return identical(existingSurface.picture, picture) ? 0.0 : 1.0;
}
@override
Matrix4 get localTransformInverse => Matrix4.identity();
@override
void build() {
super.build();
buildCount++;
}
@override
void retain() {
super.retain();
retainCount++;
}
@override
void applyPaint(EngineCanvas? oldCanvas) {
applyPaintCount++;
}
@override
void update(PersistedPicture oldSurface) {
super.update(oldSurface);
updateCount++;
}
@override
int get bitmapPixelCount => 0;
}
/// Draw 4 circles within 50, 50, 120, 120 bounds
ui.Picture _drawPicture() {
const double offsetX = 50;
const double offsetY = 50;
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 400, 400));
final ui.Shader gradient = ui.Gradient.radial(const ui.Offset(100, 100), 50, const <ui.Color>[
ui.Color.fromARGB(255, 0, 0, 0),
ui.Color.fromARGB(255, 0, 0, 255),
]);
canvas.drawCircle(
const ui.Offset(offsetX + 10, offsetY + 10),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..shader = gradient,
);
canvas.drawCircle(
const ui.Offset(offsetX + 60, offsetY + 10),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(255, 0, 0, 1),
);
canvas.drawCircle(
const ui.Offset(offsetX + 10, offsetY + 60),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(0, 255, 0, 1),
);
canvas.drawCircle(
const ui.Offset(offsetX + 60, offsetY + 60),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(0, 0, 255, 1),
);
return recorder.endRecording();
}
EnginePicture _drawEmptyPicture() {
final EnginePictureRecorder recorder = EnginePictureRecorder();
recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 400, 400));
return recorder.endRecording();
}
EnginePicture _drawPathImagePath() {
const double offsetX = 50;
const double offsetY = 50;
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 400, 400));
final ui.Shader gradient = ui.Gradient.radial(const ui.Offset(100, 100), 50, const <ui.Color>[
ui.Color.fromARGB(255, 0, 0, 0),
ui.Color.fromARGB(255, 0, 0, 255),
]);
canvas.drawCircle(
const ui.Offset(offsetX + 10, offsetY + 10),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..shader = gradient,
);
canvas.drawCircle(
const ui.Offset(offsetX + 60, offsetY + 10),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(255, 0, 0, 1),
);
canvas.drawCircle(
const ui.Offset(offsetX + 10, offsetY + 60),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(0, 255, 0, 1),
);
canvas.drawImage(createTestImage(), ui.Offset.zero, SurfacePaint());
canvas.drawCircle(
const ui.Offset(offsetX + 10, offsetY + 10),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..shader = gradient,
);
canvas.drawCircle(
const ui.Offset(offsetX + 60, offsetY + 60),
10,
SurfacePaint()
..style = ui.PaintingStyle.fill
..color = const ui.Color.fromRGBO(0, 0, 255, 1),
);
return recorder.endRecording();
}
HtmlImage createTestImage({int width = 100, int height = 50}) {
final DomCanvasElement canvas = createDomCanvasElement(width: width, height: height);
final DomCanvasRenderingContext2D 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();
final DomHTMLImageElement imageElement = createDomHTMLImageElement();
imageElement.src = js_util.callMethod<String>(canvas, 'toDataURL', <dynamic>[]);
return HtmlImage(imageElement, width, height);
}
class SceneTester {
SceneTester(this.scene);
final SurfaceScene scene;
void expectSceneHtml(String expectedHtml) {
expect(scene.webOnlyRootElement, hasHtml(expectedHtml));
}
}

View File

@@ -1,133 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/html/shaders/normalized_gradient.dart';
import 'package:ui/ui.dart' as ui hide window;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('Shader Normalized Gradient', () {
test('3 stop at start', () {
final NormalizedGradient gradient = NormalizedGradient(
const <ui.Color>[ui.Color(0xFF000000), ui.Color(0xFFFF7f3f)],
stops: <double>[0.0, 0.5],
);
int res = _computeColorAt(gradient, 0.0);
assert(res == 0xFF000000);
res = _computeColorAt(gradient, 0.25);
assert(res == 0xFF7f3f1f);
res = _computeColorAt(gradient, 0.5);
assert(res == 0xFFFF7f3f);
res = _computeColorAt(gradient, 0.7);
assert(res == 0xFFFF7f3f);
res = _computeColorAt(gradient, 1.0);
assert(res == 0xFFFF7f3f);
});
test('3 stop at end', () {
final NormalizedGradient gradient = NormalizedGradient(
const <ui.Color>[ui.Color(0xFF000000), ui.Color(0xFFFF7f3f)],
stops: <double>[0.5, 1.0],
);
int res = _computeColorAt(gradient, 0.0);
assert(res == 0xFF000000);
res = _computeColorAt(gradient, 0.25);
assert(res == 0xFF000000);
res = _computeColorAt(gradient, 0.5);
assert(res == 0xFF000000);
res = _computeColorAt(gradient, 0.75);
assert(res == 0xFF7f3f1f);
res = _computeColorAt(gradient, 1.0);
assert(res == 0xFFFF7f3f);
});
test('4 stop', () {
final NormalizedGradient gradient = NormalizedGradient(
const <ui.Color>[ui.Color(0xFF000000), ui.Color(0xFFFF7f3f)],
stops: <double>[0.25, 0.5],
);
int res = _computeColorAt(gradient, 0.0);
assert(res == 0xFF000000);
res = _computeColorAt(gradient, 0.25);
assert(res == 0xFF000000);
res = _computeColorAt(gradient, 0.4);
assert(res == 0xFF994c25);
res = _computeColorAt(gradient, 0.5);
assert(res == 0xFFFF7f3f);
res = _computeColorAt(gradient, 0.75);
assert(res == 0xFFFF7f3f);
res = _computeColorAt(gradient, 1.0);
assert(res == 0xFFFF7f3f);
});
test('5 stop', () {
final NormalizedGradient gradient = NormalizedGradient(
const <ui.Color>[
ui.Color(0x10000000),
ui.Color(0x20FF0000),
ui.Color(0x4000FF00),
ui.Color(0x800000FF),
ui.Color(0xFFFFFFFF),
],
stops: <double>[0.0, 0.1, 0.2, 0.5, 1.0],
);
int res = _computeColorAt(gradient, 0.0);
assert(res == 0x10000000);
res = _computeColorAt(gradient, 0.05);
assert(res == 0x187f0000);
res = _computeColorAt(gradient, 0.1);
assert(res == 0x20ff0000);
res = _computeColorAt(gradient, 0.15);
assert(res == 0x307f7f00);
res = _computeColorAt(gradient, 0.2);
assert(res == 0x4000ff00);
res = _computeColorAt(gradient, 0.4);
assert(res == 0x6a0054a9);
res = _computeColorAt(gradient, 0.5);
assert(res == 0x800000fe);
res = _computeColorAt(gradient, 0.9);
assert(res == 0xe5ccccff);
res = _computeColorAt(gradient, 1.0);
assert(res == 0xffffffff);
});
test('2 stops at ends', () {
final NormalizedGradient gradient = NormalizedGradient(const <ui.Color>[
ui.Color(0x00000000),
ui.Color(0xFFFFFFFF),
]);
int res = _computeColorAt(gradient, 0.0);
assert(res == 0);
res = _computeColorAt(gradient, 1.0);
assert(res == 0xFFFFFFFF);
res = _computeColorAt(gradient, 0.5);
assert(res == 0x7f7f7f7f);
});
});
}
int _computeColorAt(NormalizedGradient gradient, double t) {
int i = 0;
while (t > gradient.thresholdAt(i + 1)) {
++i;
}
final double r = t * gradient.scaleAt(i * 4) + gradient.biasAt(i * 4);
final double g = t * gradient.scaleAt(i * 4 + 1) + gradient.biasAt(i * 4 + 1);
final double b = t * gradient.scaleAt(i * 4 + 2) + gradient.biasAt(i * 4 + 2);
final double a = t * gradient.scaleAt(i * 4 + 3) + gradient.biasAt(i * 4 + 3);
int val = 0;
val |= (a * 0xFF).toInt() & 0xFF;
val <<= 8;
val |= (r * 0xFF).toInt() & 0xFF;
val <<= 8;
val |= (g * 0xFF).toInt() & 0xFF;
val <<= 8;
val |= (b * 0xFF).toInt() & 0xFF;
return val;
}

View File

@@ -1,217 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../../../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
const String mat2Sample = 'mat2(1.1, 2.1, 1.2, 2.2)';
const String mat3Sample =
'mat3(1.1, 2.1, 3.1, // first column (not row!)\n'
'1.2, 2.2, 3.2, // second column\n'
'1.3, 2.3, 3.3 // third column\n'
')';
const String mat4Sample =
'mat3(1.1, 2.1, 3.1, 4.1,\n'
'1.2, 2.2, 3.2, 4.2,\n'
'1.3, 2.3, 3.3, 4.3,\n'
'1.4, 2.4, 3.4, 4.4,\n'
')';
setUpAll(() async {
await bootstrapAndRunApp();
});
group('Shader Declarations', () {
test('Constant declaration WebGL1', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl1);
builder.addConst(ShaderType.kBool, 'false');
builder.addConst(ShaderType.kInt, '0');
builder.addConst(ShaderType.kFloat, '1.0');
builder.addConst(ShaderType.kBVec2, 'bvec2(false, false)');
builder.addConst(ShaderType.kBVec3, 'bvec3(false, false, true)');
builder.addConst(ShaderType.kBVec4, 'bvec4(true, true, false, false)');
builder.addConst(ShaderType.kIVec2, 'ivec2(1, 2)');
builder.addConst(ShaderType.kIVec3, 'ivec3(1, 2, 3)');
builder.addConst(ShaderType.kIVec4, 'ivec4(1, 2, 3, 4)');
builder.addConst(ShaderType.kVec2, 'vec2(1.0, 2.0)');
builder.addConst(ShaderType.kVec3, 'vec3(1.0, 2.0, 3.0)');
builder.addConst(ShaderType.kVec4, 'vec4(1.0, 2.0, 3.0, 4.0)');
builder.addConst(ShaderType.kMat2, mat2Sample);
builder.addConst(ShaderType.kMat2, mat2Sample, name: 'transform1');
builder.addConst(ShaderType.kMat3, mat3Sample);
builder.addConst(ShaderType.kMat4, mat4Sample);
expect(
builder.build(),
'const bool c_0 = false;\n'
'const int c_1 = 0;\n'
'const float c_2 = 1.0;\n'
'const bvec2 c_3 = bvec2(false, false);\n'
'const bvec3 c_4 = bvec3(false, false, true);\n'
'const bvec4 c_5 = bvec4(true, true, false, false);\n'
'const ivec2 c_6 = ivec2(1, 2);\n'
'const ivec3 c_7 = ivec3(1, 2, 3);\n'
'const ivec4 c_8 = ivec4(1, 2, 3, 4);\n'
'const vec2 c_9 = vec2(1.0, 2.0);\n'
'const vec3 c_10 = vec3(1.0, 2.0, 3.0);\n'
'const vec4 c_11 = vec4(1.0, 2.0, 3.0, 4.0);\n'
'const mat2 c_12 = $mat2Sample;\n'
'const mat2 transform1 = $mat2Sample;\n'
'const mat3 c_13 = $mat3Sample;\n'
'const mat4 c_14 = $mat4Sample;\n',
);
});
test('Constant declaration WebGL2', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2);
builder.addConst(ShaderType.kBool, 'false');
builder.addConst(ShaderType.kInt, '0');
builder.addConst(ShaderType.kFloat, '1.0');
builder.addConst(ShaderType.kBVec2, 'bvec2(false, false)');
builder.addConst(ShaderType.kBVec3, 'bvec3(false, false, true)');
builder.addConst(ShaderType.kBVec4, 'bvec4(true, true, false, false)');
builder.addConst(ShaderType.kIVec2, 'ivec2(1, 2)');
builder.addConst(ShaderType.kIVec3, 'ivec3(1, 2, 3)');
builder.addConst(ShaderType.kIVec4, 'ivec4(1, 2, 3, 4)');
builder.addConst(ShaderType.kVec2, 'vec2(1.0, 2.0)');
builder.addConst(ShaderType.kVec3, 'vec3(1.0, 2.0, 3.0)');
builder.addConst(ShaderType.kVec4, 'vec4(1.0, 2.0, 3.0, 4.0)');
builder.addConst(ShaderType.kMat2, mat2Sample);
builder.addConst(ShaderType.kMat2, mat2Sample, name: 'transform2');
builder.addConst(ShaderType.kMat3, mat3Sample);
builder.addConst(ShaderType.kMat4, mat4Sample);
expect(
builder.build(),
'#version 300 es\n'
'const bool c_0 = false;\n'
'const int c_1 = 0;\n'
'const float c_2 = 1.0;\n'
'const bvec2 c_3 = bvec2(false, false);\n'
'const bvec3 c_4 = bvec3(false, false, true);\n'
'const bvec4 c_5 = bvec4(true, true, false, false);\n'
'const ivec2 c_6 = ivec2(1, 2);\n'
'const ivec3 c_7 = ivec3(1, 2, 3);\n'
'const ivec4 c_8 = ivec4(1, 2, 3, 4);\n'
'const vec2 c_9 = vec2(1.0, 2.0);\n'
'const vec3 c_10 = vec3(1.0, 2.0, 3.0);\n'
'const vec4 c_11 = vec4(1.0, 2.0, 3.0, 4.0);\n'
'const mat2 c_12 = $mat2Sample;\n'
'const mat2 transform2 = $mat2Sample;\n'
'const mat3 c_13 = $mat3Sample;\n'
'const mat4 c_14 = $mat4Sample;\n',
);
});
test('Attribute declaration WebGL1', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl1);
builder.addIn(ShaderType.kVec4, name: 'position');
builder.addIn(ShaderType.kVec4);
expect(
builder.build(),
'attribute vec4 position;\n'
'attribute vec4 attr_0;\n',
);
});
test('in declaration WebGL1', () {
final ShaderBuilder builder = ShaderBuilder.fragment(WebGLVersion.webgl1);
builder.addIn(ShaderType.kVec4, name: 'position');
builder.addIn(ShaderType.kVec4);
expect(
builder.build(),
'varying vec4 position;\n'
'varying vec4 attr_0;\n',
);
});
test('Attribute declaration WebGL2', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2);
builder.addIn(ShaderType.kVec4, name: 'position');
builder.addIn(ShaderType.kVec4);
expect(
builder.build(),
'#version 300 es\n'
'in vec4 position;\n'
'in vec4 attr_0;\n',
);
});
test('Uniform declaration WebGL1', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl1);
final ShaderDeclaration variable = builder.addUniform(ShaderType.kVec4, name: 'v1');
expect(variable.name, 'v1');
expect(variable.dataType, ShaderType.kVec4);
expect(variable.storage, ShaderStorageQualifier.kUniform);
builder.addUniform(ShaderType.kVec4);
expect(
builder.build(),
'uniform vec4 v1;\n'
'uniform vec4 uni_0;\n',
);
});
test('Uniform declaration WebGL2', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2);
final ShaderDeclaration variable = builder.addUniform(ShaderType.kVec4, name: 'v1');
expect(variable.name, 'v1');
expect(variable.dataType, ShaderType.kVec4);
expect(variable.storage, ShaderStorageQualifier.kUniform);
builder.addUniform(ShaderType.kVec4);
expect(
builder.build(),
'#version 300 es\n'
'uniform vec4 v1;\n'
'uniform vec4 uni_0;\n',
);
});
test('Float precision', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2);
builder.floatPrecision = ShaderPrecision.kLow;
builder.addUniform(ShaderType.kFloat, name: 'f1');
expect(
builder.build(),
'#version 300 es\n'
'precision lowp float;\n'
'uniform float f1;\n',
);
});
test('Integer precision', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2);
builder.integerPrecision = ShaderPrecision.kLow;
builder.addUniform(ShaderType.kInt, name: 'i1');
expect(
builder.build(),
'#version 300 es\n'
'precision lowp int;\n'
'uniform int i1;\n',
);
});
test('Method', () {
final ShaderBuilder builder = ShaderBuilder(WebGLVersion.webgl2);
builder.floatPrecision = ShaderPrecision.kMedium;
final ShaderDeclaration variable = builder.addUniform(ShaderType.kFloat, name: 'f1');
final ShaderMethod m = builder.addMethod('main');
m.addStatement('f1 = 5.0;');
expect(
builder.build(),
'#version 300 es\n'
'precision mediump float;\n'
'uniform float ${variable.name};\n'
'void main() {\n'
' f1 = 5.0;\n'
'}\n',
);
});
});
}

View File

@@ -1,502 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../../common/test_initialization.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('Surface', () {
setUpAll(() async {
await bootstrapAndRunApp(withImplicitView: true);
});
setUp(() {
SurfaceSceneBuilder.debugForgetFrameScene();
});
test('debugAssertSurfaceState produces a human-readable message', () {
final SceneBuilder builder = SceneBuilder();
final PersistedOpacity opacityLayer = builder.pushOpacity(100) as PersistedOpacity;
try {
debugAssertSurfaceState(
opacityLayer,
PersistedSurfaceState.active,
PersistedSurfaceState.pendingRetention,
);
fail('Expected $PersistedSurfaceException');
} on PersistedSurfaceException catch (exception) {
expect(
'$exception',
'PersistedOpacity: is in an unexpected state.\n'
'Expected one of: PersistedSurfaceState.active, PersistedSurfaceState.pendingRetention\n'
'But was: PersistedSurfaceState.created',
);
}
});
test('is created', () {
final SceneBuilder builder = SceneBuilder();
final PersistedOpacity opacityLayer = builder.pushOpacity(100) as PersistedOpacity;
builder.pop();
expect(opacityLayer, isNotNull);
expect(opacityLayer.rootElement, isNull);
expect(opacityLayer.isCreated, isTrue);
builder.build();
expect(opacityLayer.rootElement!.tagName.toLowerCase(), 'flt-opacity');
expect(opacityLayer.isActive, isTrue);
});
test('is released', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer = builder1.pushOpacity(100) as PersistedOpacity;
builder1.pop();
builder1.build();
expect(opacityLayer.isActive, isTrue);
SceneBuilder().build();
expect(opacityLayer.isReleased, isTrue);
expect(opacityLayer.rootElement, isNull);
});
test('discarding is recursive', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer = builder1.pushOpacity(100) as PersistedOpacity;
final PersistedTransform transformLayer =
builder1.pushTransform(Matrix4.identity().toFloat64()) as PersistedTransform;
builder1.pop();
builder1.pop();
builder1.build();
expect(opacityLayer.isActive, isTrue);
expect(transformLayer.isActive, isTrue);
SceneBuilder().build();
expect(opacityLayer.isReleased, isTrue);
expect(transformLayer.isReleased, isTrue);
expect(opacityLayer.rootElement, isNull);
expect(transformLayer.rootElement, isNull);
});
test('is updated', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer1 = builder1.pushOpacity(100) as PersistedOpacity;
builder1.pop();
builder1.build();
expect(opacityLayer1.isActive, isTrue);
final DomElement element = opacityLayer1.rootElement!;
final SceneBuilder builder2 = SceneBuilder();
final PersistedOpacity opacityLayer2 =
builder2.pushOpacity(200, oldLayer: opacityLayer1) as PersistedOpacity;
expect(opacityLayer1.isPendingUpdate, isTrue);
expect(opacityLayer2.isCreated, isTrue);
expect(opacityLayer2.oldLayer, same(opacityLayer1));
builder2.pop();
builder2.build();
expect(opacityLayer1.isReleased, isTrue);
expect(opacityLayer1.rootElement, isNull);
expect(opacityLayer2.isActive, isTrue);
expect(opacityLayer2.rootElement, element); // adopts old surface's element
expect(opacityLayer2.oldLayer, isNull);
});
test('ignores released surface when updated', () {
// Build a surface
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer1 = builder1.pushOpacity(100) as PersistedOpacity;
builder1.pop();
builder1.build();
expect(opacityLayer1.isActive, isTrue);
final DomElement element = opacityLayer1.rootElement!;
// Release it
SceneBuilder().build();
expect(opacityLayer1.isReleased, isTrue);
expect(opacityLayer1.rootElement, isNull);
// Attempt to update it
final SceneBuilder builder2 = SceneBuilder();
final PersistedOpacity opacityLayer2 =
builder2.pushOpacity(200, oldLayer: opacityLayer1) as PersistedOpacity;
builder2.pop();
expect(opacityLayer1.isReleased, isTrue);
expect(opacityLayer2.isCreated, isTrue);
builder2.build();
expect(opacityLayer1.isReleased, isTrue);
expect(opacityLayer2.isActive, isTrue);
expect(opacityLayer2.rootElement, isNot(equals(element)));
});
// This test creates a situation when an intermediate layer disappears,
// causing its child to become a direct child of the common ancestor. This
// often happens with opacity layers. When opacity reaches 1.0, the
// framework removes that layer (as it is no longer necessary). This test
// makes sure we reuse the child layer's DOM nodes. Here's the illustration
// of what's happening:
//
// Frame 1 Frame 2
//
// A A
// | |
// B ┌──>C
// | │ |
// C ────┘ L
// |
// L
//
// Layer "L" is a logging layer used to track what would happen to the
// child of "C" as it's being dragged around the tree. For example, we
// check that the child doesn't get discarded by mistake.
test(
'reparents DOM element when updated',
() {
final _LoggingTestSurface logger = _LoggingTestSurface();
final SurfaceSceneBuilder builder1 = SurfaceSceneBuilder();
final PersistedTransform a1 =
builder1.pushTransform(
(Matrix4.identity()..scale(EngineFlutterDisplay.instance.browserDevicePixelRatio))
.toFloat64(),
)
as PersistedTransform;
final PersistedOpacity b1 = builder1.pushOpacity(100) as PersistedOpacity;
final PersistedTransform c1 =
builder1.pushTransform(Matrix4.identity().toFloat64()) as PersistedTransform;
builder1.debugAddSurface(logger);
builder1.pop();
builder1.pop();
builder1.pop();
builder1.build();
expect(logger.log, <String>['build', 'createElement', 'apply']);
final DomElement elementA = a1.rootElement!;
final DomElement elementB = b1.rootElement!;
final DomElement elementC = c1.rootElement!;
expect(elementC.parent, elementB);
expect(elementB.parent, elementA);
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
final PersistedTransform a2 =
builder2.pushTransform(
(Matrix4.identity()..scale(EngineFlutterDisplay.instance.browserDevicePixelRatio))
.toFloat64(),
oldLayer: a1,
)
as PersistedTransform;
final PersistedTransform c2 =
builder2.pushTransform(Matrix4.identity().toFloat64(), oldLayer: c1)
as PersistedTransform;
builder2.addRetained(logger);
builder2.pop();
builder2.pop();
expect(c1.isPendingUpdate, isTrue);
expect(c2.isCreated, isTrue);
builder2.build();
expect(logger.log, <String>['build', 'createElement', 'apply', 'retain']);
expect(c1.isReleased, isTrue);
expect(c2.isActive, isTrue);
expect(a2.rootElement, elementA);
expect(b1.rootElement, isNull);
expect(c2.rootElement, elementC);
expect(elementC.parent, elementA);
expect(elementB.parent, null);
},
// This method failed on iOS Safari.
// TODO(ferhat): https://github.com/flutter/flutter/issues/60036
skip:
ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit &&
ui_web.browser.operatingSystem == ui_web.OperatingSystem.iOs,
);
test('is retained', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer = builder1.pushOpacity(100) as PersistedOpacity;
builder1.pop();
builder1.build();
expect(opacityLayer.isActive, isTrue);
final DomElement element = opacityLayer.rootElement!;
final SceneBuilder builder2 = SceneBuilder();
expect(opacityLayer.isActive, isTrue);
builder2.addRetained(opacityLayer);
expect(opacityLayer.isPendingRetention, isTrue);
builder2.build();
expect(opacityLayer.isActive, isTrue);
expect(opacityLayer.rootElement, element);
});
test('revives released surface when retained', () {
final SurfaceSceneBuilder builder1 = SurfaceSceneBuilder();
final PersistedOpacity opacityLayer = builder1.pushOpacity(100) as PersistedOpacity;
final _LoggingTestSurface logger = _LoggingTestSurface();
builder1.debugAddSurface(logger);
builder1.pop();
builder1.build();
expect(opacityLayer.isActive, isTrue);
expect(logger.log, <String>['build', 'createElement', 'apply']);
final DomElement element = opacityLayer.rootElement!;
SceneBuilder().build();
expect(opacityLayer.isReleased, isTrue);
expect(opacityLayer.rootElement, isNull);
expect(logger.log, <String>['build', 'createElement', 'apply', 'discard']);
final SceneBuilder builder2 = SceneBuilder();
builder2.addRetained(opacityLayer);
expect(opacityLayer.isCreated, isTrue); // revived
expect(logger.log, <String>['build', 'createElement', 'apply', 'discard', 'revive']);
builder2.build();
expect(opacityLayer.isActive, isTrue);
expect(opacityLayer.rootElement, isNot(equals(element)));
});
test('reviving is recursive', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer = builder1.pushOpacity(100) as PersistedOpacity;
final PersistedTransform transformLayer =
builder1.pushTransform(Matrix4.identity().toFloat64()) as PersistedTransform;
builder1.pop();
builder1.pop();
builder1.build();
expect(opacityLayer.isActive, isTrue);
expect(transformLayer.isActive, isTrue);
final DomElement opacityElement = opacityLayer.rootElement!;
final DomElement transformElement = transformLayer.rootElement!;
SceneBuilder().build();
final SceneBuilder builder2 = SceneBuilder();
builder2.addRetained(opacityLayer);
expect(opacityLayer.isCreated, isTrue); // revived
expect(transformLayer.isCreated, isTrue); // revived
builder2.build();
expect(opacityLayer.isActive, isTrue);
expect(transformLayer.isActive, isTrue);
expect(opacityLayer.rootElement, isNot(equals(opacityElement)));
expect(transformLayer.rootElement, isNot(equals(transformElement)));
});
// This test creates a situation when a retained layer is moved to another
// parent. We want to make sure that we move the retained layer's elements
// without rebuilding from scratch. No new elements are created in this
// situation.
//
// Here's an illustrated example where layer C is reparented onto B along
// with D:
//
// Frame 1 Frame 2
//
// A A
// ╲ |
// B C ──┐ B
// | │ |
// D └──>C
// |
// D
test('reparents DOM elements when retained', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity a1 = builder1.pushOpacity(10) as PersistedOpacity;
final PersistedOpacity b1 = builder1.pushOpacity(20) as PersistedOpacity;
builder1.pop();
final PersistedOpacity c1 = builder1.pushOpacity(30) as PersistedOpacity;
final PersistedOpacity d1 = builder1.pushOpacity(40) as PersistedOpacity;
builder1.pop();
builder1.pop();
builder1.pop();
builder1.build();
final DomElement elementA = a1.rootElement!;
final DomElement elementB = b1.rootElement!;
final DomElement elementC = c1.rootElement!;
final DomElement elementD = d1.rootElement!;
expect(elementB.parent, elementA);
expect(elementC.parent, elementA);
expect(elementD.parent, elementC);
final SceneBuilder builder2 = SceneBuilder();
final PersistedOpacity a2 = builder2.pushOpacity(10, oldLayer: a1) as PersistedOpacity;
final PersistedOpacity b2 = builder2.pushOpacity(20, oldLayer: b1) as PersistedOpacity;
builder2.addRetained(c1);
builder2.pop();
builder2.pop();
builder2.build();
expect(a2.rootElement, elementA);
expect(b2.rootElement, elementB);
expect(c1.rootElement, elementC);
expect(d1.rootElement, elementD);
expect(
<DomElement>[elementD.parent!, elementC.parent!, elementB.parent!],
<DomElement>[elementC, elementB, elementA],
);
});
test('is updated by matching', () {
final SceneBuilder builder1 = SceneBuilder();
final PersistedOpacity opacityLayer1 = builder1.pushOpacity(100) as PersistedOpacity;
builder1.pop();
builder1.build();
expect(opacityLayer1.isActive, isTrue);
final DomElement element = opacityLayer1.rootElement!;
final SceneBuilder builder2 = SceneBuilder();
final PersistedOpacity opacityLayer2 = builder2.pushOpacity(200) as PersistedOpacity;
expect(opacityLayer1.isActive, isTrue);
expect(opacityLayer2.isCreated, isTrue);
builder2.pop();
builder2.build();
expect(opacityLayer1.isReleased, isTrue);
expect(opacityLayer1.rootElement, isNull);
expect(opacityLayer2.isActive, isTrue);
expect(opacityLayer2.rootElement, element); // adopts old surface's element
});
});
final Map<String, TestEngineLayerFactory> layerFactories = <String, TestEngineLayerFactory>{
'ColorFilterEngineLayer':
(SurfaceSceneBuilder builder) =>
builder.pushColorFilter(const ColorFilter.mode(Color(0xFFFF0000), BlendMode.srcIn)),
'OffsetEngineLayer': (SurfaceSceneBuilder builder) => builder.pushOffset(1, 2),
'TransformEngineLayer':
(SurfaceSceneBuilder builder) => builder.pushTransform(Matrix4.identity().toFloat64()),
'ClipRectEngineLayer':
(SurfaceSceneBuilder builder) => builder.pushClipRect(const Rect.fromLTRB(0, 0, 10, 10)),
'ClipRRectEngineLayer':
(SurfaceSceneBuilder builder) =>
builder.pushClipRRect(RRect.fromRectXY(const Rect.fromLTRB(0, 0, 10, 10), 1, 2)),
'ClipPathEngineLayer':
(SurfaceSceneBuilder builder) =>
builder.pushClipPath(Path()..addRect(const Rect.fromLTRB(0, 0, 10, 10))),
'OpacityEngineLayer': (SurfaceSceneBuilder builder) => builder.pushOpacity(100),
'ImageFilterEngineLayer':
(SurfaceSceneBuilder builder) =>
builder.pushImageFilter(ImageFilter.blur(sigmaX: 0.1, sigmaY: 0.2)),
'BackdropEngineLayer':
(SurfaceSceneBuilder builder) =>
builder.pushBackdropFilter(ImageFilter.blur(sigmaX: 0.1, sigmaY: 0.2)),
// Firefox does not support WebGL in headless mode.
if (!isFirefox)
'ShaderMaskEngineLayer': (SurfaceSceneBuilder builder) {
const List<Color> colors = <Color>[Color(0xFF000000), Color(0xFFFF3C38)];
const List<double> stops = <double>[0.0, 1.0];
const Rect shaderBounds = Rect.fromLTWH(180, 10, 140, 140);
final EngineGradient shader = GradientLinear(
Offset(200 - shaderBounds.left, 30 - shaderBounds.top),
Offset(320 - shaderBounds.left, 150 - shaderBounds.top),
colors,
stops,
TileMode.clamp,
Matrix4.identity().storage,
);
return builder.pushShaderMask(shader, shaderBounds, BlendMode.srcOver);
},
};
// Regression test for https://github.com/flutter/flutter/issues/104305
for (final MapEntry<String, TestEngineLayerFactory> layerFactory in layerFactories.entries) {
test('${layerFactory.key} supports addRetained after being discarded', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushOffset(0, 0);
final PersistedSurface oldLayer = layerFactory.value(builder) as PersistedSurface;
builder.pop();
builder.pop();
builder.build();
expect(oldLayer.isActive, isTrue);
// Pump an empty frame so the `oldLayer` is discarded before it's reused.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.build();
expect(oldLayer.isReleased, isTrue);
// At this point the `oldLayer` needs to be revived.
final SurfaceSceneBuilder builder3 = SurfaceSceneBuilder();
builder3.addRetained(oldLayer);
builder3.build();
expect(oldLayer.isActive, isTrue);
});
}
}
typedef TestEngineLayerFactory = EngineLayer Function(SurfaceSceneBuilder builder);
class _LoggingTestSurface extends PersistedContainerSurface {
_LoggingTestSurface() : super(null);
final List<String> log = <String>[];
@override
void build() {
log.add('build');
super.build();
}
@override
void apply() {
log.add('apply');
}
@override
DomElement createElement() {
log.add('createElement');
return createDomElement('flt-test-layer');
}
@override
void update(_LoggingTestSurface oldSurface) {
log.add('update');
super.update(oldSurface);
}
@override
void adoptElements(covariant PersistedSurface oldSurface) {
log.add('adoptElements');
super.adoptElements(oldSurface);
}
@override
void retain() {
log.add('retain');
super.retain();
}
@override
void discard() {
log.add('discard');
super.discard();
}
@override
void revive() {
log.add('revive');
super.revive();
}
@override
double matchForUpdate(PersistedSurface? existingSurface) {
return 1.0;
}
}

View File

@@ -1,122 +0,0 @@
// 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/src/engine.dart';
/// 50x50 pixel flutter logo image that contains alpha ramps and colors
/// specifically to transparency and blending.
const String _flutterLogoBase64 =
'iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAKRlWElm'
'TU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgExAAIAAAAg'
'AAAAWodpAAQAAAABAAAAegAAAAAAAABIAAAAAQAAAEgAAAABQWRvYmUgUGhvdG9zaG9wIENT'
'NiAoTWFjaW50b3NoKQAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAMqADAAQAAAABAAAAMgAA'
'AABWBXsWAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEemlUWHRYTUw6Y29tLmFkb2JlLnhtcAAA'
'AAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENv'
'cmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5'
'OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjph'
'Ym91dD0iIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94'
'YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5j'
'b20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnhtcD0i'
'aHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0i'
'aHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8eG1wTU06SW5zdGFu'
'Y2VJRD54bXAuaWlkOjMyOERERjc5ODRCRjExRUE5QUE4OEM5NTZDREM5QkUyPC94bXBNTTp'
'JbnN0YW5jZUlEPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOjMyOERERj'
'dBODRCRjExRUE5QUE4OEM5NTZDREM5QkUyPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgI'
'CA8eG1wTU06T3JpZ2luYWxEb2N1bWVudElEPnhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMTgy'
'MkFBQjU0OEFBMDMwM0E8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHht'
'cE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICA'
'gPHN0UmVmOmluc3RhbmNlSUQ+eG1wLmlpZDowNDgwMTE3NDA3MjA2ODExODIyQUFCNTQ4QU'
'EwMzAzQTwvc3RSZWY6aW5zdGFuY2VJRD4KICAgICAgICAgICAgPHN0UmVmOmRvY3VtZW50SU'
'Q+eG1wLmRpZDowMTgwMTE3NDA3MjA2ODExODIyQUFCNTQ4QUEwMzAzQTwvc3RSZWY6ZG9jd'
'W1lbnRJRD4KICAgICAgICAgPC94bXBNTTpEZXJpdmVkRnJvbT4KICAgICAgICAgPHhtcDpD'
'cmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpPC94bXA6Q3JlYXRv'
'clRvb2w+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb2'
'4+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhP'
'gr/+ApQAAAQNUlEQVRoBbVaDYxdRRWemTv3vt2WbrctpRYxtCVU4io1LomElrjYWnbbgtr2'
'CbISwGpR0UBUjD8YHyZCNCqIP5GmtLWSAn3SVYqw7PbnFUQFXGOMRUFlW9G2gKalu/veu38'
'zfufce3ff275dWtBJ5s7cuTPnnO+cM2d+3pN2urDiVSHEVCFuOpgT35vVIuaLV8Tgtw8Icf'
'PZkZgntDggrhNCbhHCuihD9J402Y4OLUulqNy1+iwvFIscaaqRMWqyQRofIxFH2ohpwqjDc'
't/OZyyYSTAVHQUtSoVIfPiL7xWu3i2i0KA7PnFmstpaEVMLBnHm1rFHhEbiQZ9PKtl83pPF'
'YnB0xVVne2HlKcdz5wgTC62cZLwlGdJUSxXtWnngFItIOhdzj3xeiWIxzrpPVmpAZg4EJjc'
'tAcmqazqNxjnJVwEOnKjDhMmuX+/KDRuCoeVXn9EcDf/K0c4cEwYBBmgBjSUJZVbnJiuh9'
'1BZ4ZnYREpNucgtPfAMW7VYjHjM7Ldlg1MaJxYaxlnNzVCUP4THrLRTdRiVWVaEcQ54fpu2'
'JoTTl9rid+3tBCL8a1d3S1M0/ARAnGWiyEcfjK+xaB0Icg0ZQXGekEoo4V0qdwNEatVR+q8'
'8O6kCqZ+Wx0QPD6jgeTop72Xxd26Yx0/xYlJAFmJa4xdZO74kcwIgPpObF//rce3qhSYMAw'
'zIpaqsEYSqaE0Kcmso04F/q/fr/uKeE0AQm5OwCCwqvCzn90MzMHEbsijsJ4f1RBuysFCa'
'TGUaA0C1bGKjKufFh/Zo111kopAsQXQnTABAvu9IR6OiunX/jocagpiQQv0HDYJkhiS1Jc'
'V+Lupe0g71BRg7mNjsbmHnml7t6IswJ3wog9xpggR4Uhh4mFTapTjwCb17xzbblgSJCQa9'
'ZvOkIXGy0SkIjihh5+odcKfl6cSeBAQoSrinhYm1qwDi887uHXdbml/7i2MKnYzxBN9eFx'
'ArCgqWYBDBpWu2wp0+aAKOTll0m4AdQbBGuZ4DELcCxHfINcXAwIRBhAmdxGQ/ZSAEQogC'
'0w861/7Q9dyrGQTNqwkTzxaAEBFAaBOa7zq7dhTIqqJUogk/2XQ6uck+Ie8GH9i7O0oKjI3'
'ftfabAPEpE/i00mPFz1IDmZKmUHmua6J4g7O753NMq9hGobDBADRaK7NcaJuNbkLMoaV5gn'
'RqFunocGjr4XeuvcXT7heMXyWXIPLMiHk0FEsEsIQXh/F9zq6fX8/9CgWsxYWG4Zy/1zzmz'
'n2e6U9VpMPGacIP47vbdqzaAxvCYEX+RtfRd2Jix8IaGq/qdJoteCkBdPCldnNxFO9EiL2'
'cmmsDRdrtJIv3X+Rdc/6TQRiQAoj3qPyTGGuMNhhj/7QhCDrz61zlEAjonbwCIGrTOBD4xC'
'BsFO3R/T2vCQKupCSiGkpsYcU25NORg7Q9eu6Fg7POu+lOMWN6kxyB65EUWXpNIAmIYlBen'
'r/CddRGG4UEAiHUJrvAGmIZ0bQMEGJhiehpZ/Gi94n+nmTxzPZP4zqnr5mGSa7lyE3UDnB'
'UiJmtrUJUA+u2NktaySEHPTjVazRrTUvb1ZWjnWz10vzKZi3vp9ULoacxiHpr+ADhYa/1p'
'8PD8zpkoWDYNTG/xrEY/5pJRuWx9GNoDBhjl18ul7EJULzFoBBYmyYEwpZ49FG/0pV/T07Z'
'h0l+YwlJaolaKmk9VWegHJ1DdHpBTZ1+8Vt+c0eFN5SYXw2GTNREpNJ9P0qJzVgSVFDWA8g'
'INASSudPIijUXuNbsojkFrYR1IGrppdZAE0A4HhR4WLkti+XPtxwjWhcMDJwKiEy2sTLlVc'
'uyZp5zvxPmSAbC78q/TRtbgiV1DEcHYneU0GgFNACC1IemUCkFEOaoEqctlo9sOZLRGpPo/'
'1ers0jG+Njy/HzHmCeUElPh5yE6aZadHg1B4GCklItwU45k08Wy76eDGa3/jei1TDOK9W2'
'jQHgyYmIPL/3wnGkqfsJx5EwTxwECRmKJ+nFsCSKJ5gjLFIGIY6kvyT123/4sSOCjPJVcEi'
'WW55g4lk63TOjXLnlgttgd7fhAa5OuPgk/fzOBwHCP3b8WBDWkcwLfAULCPSWCSW6Z91jxab'
'YEgkTBpmvMKUC5RF7CUa1VtNJ5pkGqFaT+s04ORhvCwY5rm07zjpccxznHhFEV3Wg7bngC0J'
'h6GqQxCok43SkRW3mZ7r2/lFoCIAqqIJPtx3K7fOp0QWfoydM/8Xn1S6vVzXNu9ntF75xO0Z'
'mNsbRI0mgc7vlJ9WSyjwnF1zbUfJZ3fJfWDk53URmvCQjqjmU1nc/ULUkSNzfQuMLNSGzVlb'
'qv+HA6J/y8zTsAwVv8lfHSexGx3zsim4fQPwunqI4JwHXI1wIuj5/RL5eY86f+Otj31c6mTr'
'o0mIOcbUcgDAHBZjIZnxLhHYbQe3EeWOLO6NeOWgwQw+iTA3AIkvQb8yLGQJToYAQUjo6ts0'
'73bX8guz1JLcEgVkXLtjrK6Q5tSCvz3FHLMpnkQQIlWiZvBWPpisA/duvXf3v7FtEhvp52'
'HQPCwmRWyZRB73CNDpwH7PLV/8FqBxFZ97Ryk6gpnaxg5CSkxRyaYoy4ESA28ekOtyckU0E'
'UmPqqeOkPYK6rQxMeBxtsKhOmia8k9XFWiRylWlUgbnu6+R8F0GoBH+qe5URaFiUJ91yted'
'C+2Kq+HWutMT3KUdNgPbgW+SSW8vpMExFhlkDILzt9D97F84sWu4S2gnrtqnjZ7VKpGyIT0f'
'2lQxrJMt5JO2nmCw36FmuAcEJz1/bcL7+C79ibHKM+pLTaDFWTTKPg6uoKvs2+q/p7VsMQvT'
'D1DHSukHsBJM0FIgYQ0ldStUTWfMPp+9ntPA7WJBAdogPbCBGvsku/BDG/iHu2oxhDWiRmpI'
'AYQoAO5awuqa3iKDFdhXbjTm/fjeDPboCoReNGQaA9fZdp2xgtsGH6fPbmczNGymDRZRhUQn'
'UmBKxAWxlBHxxmGmPvcPt6biENi/R0RyBKshQttUtuALvbsB/7NyihyygI0ABjChDImKoRWQ'
'FtFSy4s5zAbtvplT6O/mJADIwGBLyOAsEcI2GjRLGkAIl60gY2JCPtPMAWu9IkDBewMMcrMe'
'A3YNQK34ab2Qosejri8I+dXT2fpf50ZqfTXbttdwnEvOicaw6Yl+4oi+rLjlUkOBNHRxKYB'
'E8si0WT2iAERcZZOrA7dub2daMu8ohq7aKdvlMiRVCdAWCK8ThugyIQSfGe0IL0iUXwkRODo'
'WuZnTvL0puyCjr9Iz7QAacVN4Fbnf6eT1JHW8ANSgpiQA6EC6IFq+FyP8BN+n9eEEeiEVnF'
'tbuC8BYMUhAQiN6BLgEhzUzHyN6HcvvWEk0K2UWOlPQ2mjIgMeYtA8KXpMzeUyuTpdki2VC'
'Jicur/C+3HcWEXQlBDkN7jwDEtdSH5gWdLcgSBGK+Pfd9WBDvRph71REKBzZrB+3hqCICEF'
'YQnMAkIFDSnCvD3VsB4okFB5rX4N122A5dlA1v3OuFz0DAAuRaRBd2G811QPBR0Lmc3ayv5'
'2VldCe2UeuonUHgLqvNtnkE4hx7zkUw8T3wAdwa2ypOKwYzPoyFiV4Qh7A1qAHDmpMj6DcN'
'IH4/52/hmu+f+6hPIMg1iX6DlAEJMW+5DhdDCdciS3Od3pN8wjaeCLKbJdehB+md3alQiF'
'NLBPPtwkU4c2zGygJCxoeGcPUqcQam5VuJEPP8gDgsFoi52MNoeKmhfdt0x8q/tLwi8pvO'
'e3IonV+TnVPoG+UIkx3GQ+IYQnakOjKVaWoIhL7RTSKH2DbcPcGdUu2FC+yCc42JtkB4zB'
'U7hJ4uaAa8XIIwgdEAU4EMg+KInC/mRtq6LdLEL055VV1535l7/r0errkBVs2EaFCSiKTtD'
'AwDgbK4KzZfKYgxJMmXBpTqmmgni0PiPDvvPBk7GyH0TAwsI5TiB5w6xWAY9I8nxVEf7jZN'
'TvEWSOdwa1V//P7mPxxotwLziwWsY0EvtI5A+dhfWPqVaSfybGS6RSGjyAP/PCTmf+w2MfuM'
'aeI4lkefGKXphDmSfRhXMmAbOXOxZM2AiY+DOKYDbvKtCGBxMGOGXNJ7bGTgKeWPhH82T93'
'bdYRAEM0Bub2G/Tgu9a9kDXLJzCqoS3oHD4nfXlCX+J7mkwNCoRHuf9D9+14o7CaAyAEQX'
'Sv5VCLeBtiIMBPYLmHoaNyiDcaV4mePlje8Y9G06++5O5HzQ7HIb69d+JLm+if5DB0lKFe'
'w5HHJ75JAMEDwIZ5JPjkgoADksdgr9KAc7MeK/DVw8gDCxx4NHgTCsAATNSCsXJwsnw/K2'
'z8TlO98Z6wXRkfiYdXe3L35W0RKFAEG6xHXGz/IA+goQfdaTeBH9WY+xBnrsxUYAPFMrDQ'
'ZMYwdly7BBETIBJiHsFP+NgRvggWqWMwpI3oBmMj5Vj7nl39xvV/5xrt85/zAt1W4obWHs'
'OAvabpq0y1MFQGkARiyBCVypz3IjyD3wgMeQ/nwSLn6lMi5EWIvpocEIALFwHz2fXQ6+YR'
'whc40+eOzo7OvwvRchw0JXaZBKTn8RvycrfReJ6ufXizkhcmSaHCKhPWgPEVXSrOMUL/wt1'
'33vYQpxTuOq8nrpM8FC5u683dh0SqDP8kxKv+pWYSY0BmcNoSIZAf1wW04Nf0E95dNiMJlg'
'KgGu7v94NoLq84FcdUGsIVUsFbq7zguIigcQgC8tKl780eJXJI4eGcvXMIKKst79+7lZaJ9f'
'Xeuis0sZKAzPeaNHc2nDoTYJGBoyZD/kINbrWl60Kq/5oJf5YcrH1tStctUNdROJdJuFf'
'eSVei7CgCcMZbc5hDm1grvqs15tkbh1jrtMgtEFQrFlH/0o1fY5Q5VeIWniZ9k0ISpOb8'
'+IMyJLUM18aJ+blP49BW9w99Z5oUXNg+FWlWMlVV4DJUVSAMtphlC4NcICptHsIG9PNd9T'
'xfmCs2XE8AwcTyKbc8ykMMWgZAOfpJ3z2QZZMP59QMhLmSZNPq89O4HNsYt7uPWiTUCJB1x'
'y7AENomU+VcA/BKQvBNAjIQmASY2l+srN1+cgDnRxTIwSakoaJQ5SzpiEKAkvzEgRL0m+lS'
'3fnQjzn/PQEsKAQCLphgBGGQLUJSTd2onUABCIF9WNrrMvWLTu0QBR+zJwjJub2ENuiAhA'
'NiEZtmOvHEg48Hcv24z/ABnGUnniFeh46HxGYCGpDVDCDzD+M0Ll14KkyDuyn3knrfWKia'
'xQs2TNtcYC08YwrpFgOiaCVmB1v8ykTbZQnu19/zgSmXs6QjKFHKZD52U6pIBJPoGc+G6i'
'koVGq9PFK/5FyISxuA7p7R+7c1vEqGzAusRBQwk2j0mabSSNbzhMgPTsVe7Zx54O85TYz9b'
'G7q0QYqx43NQp5J+DyaxgBqC4d9IEn9j8Z8VxRvogo76E5ik7C7gmmjkHXjFDz64oKFxDs5'
'rMQ4IqP4fUo0219/tijMXppoFq1LKbmHySy2/PY/v9H50hhEz8KvE0RkS2xhsP8YlvvGZ3S'
'yapiT0mk/DqjIsBcr/Atffr8/hCjApAAAAAElFTkSuQmCC';
HtmlImage createFlutterLogoTestImage() {
return HtmlImage(
createDomHTMLImageElement()..src = 'data:text/plain;base64,$_flutterLogoBase64',
50,
50,
);
}

View File

@@ -20,11 +20,6 @@ void main() {
Future<void> testMain() async {
setUpUnitTests(setUpTestViewDimensions: false);
final bool deviceClipRoundsOut = renderer is! HtmlRenderer;
runCanvasTests(deviceClipRoundsOut: deviceClipRoundsOut);
}
void runCanvasTests({required bool deviceClipRoundsOut}) {
setUp(() {
EngineSemantics.debugResetSemantics();
});
@@ -150,23 +145,22 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
canvas.clipRect(clipRawBounds);
// Save initial return values for testing restored values
final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.save();
canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
// Both clip bounds have changed
rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds);
rectsNotClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
// Previous return values have not changed
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.restore();
// save/restore returned the values to their original values
@@ -178,7 +172,7 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13);
rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds);
// Destination bounds are unaffected by transform
rectsClose(canvas.getDestinationClipBounds(), clipDestBounds);
rectsClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
canvas.restore();
// save/restore returned the values to their original values
@@ -191,7 +185,6 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
final ui.RRect clip = ui.RRect.fromRectAndRadius(clipRawBounds, const ui.Radius.circular(3));
canvas.clipRRect(clip);
@@ -199,16 +192,16 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.save();
canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
// Both clip bounds have changed
rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds);
rectsNotClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
// Previous return values have not changed
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.restore();
// save/restore returned the values to their original values
@@ -220,7 +213,7 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13);
rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds);
// Destination bounds are unaffected by transform
rectsClose(canvas.getDestinationClipBounds(), clipDestBounds);
rectsClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
canvas.restore();
// save/restore returned the values to their original values
@@ -233,7 +226,6 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
final ui.Path clip =
ui.Path()
..addRect(clipRawBounds)
@@ -244,16 +236,16 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.save();
canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
// Both clip bounds have changed
rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds);
rectsNotClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
// Previous return values have not changed
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.restore();
// save/restore returned the values to their original values
@@ -265,7 +257,7 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13);
rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds);
// Destination bounds are unaffected by transform
rectsClose(canvas.getDestinationClipBounds(), clipDestBounds);
rectsClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
canvas.restore();
// save/restore returned the values to their original values
@@ -278,14 +270,13 @@ void runCanvasTests({required bool deviceClipRoundsOut}) {
final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
canvas.clipRect(clipRawBounds);
// Save initial return values for testing restored values
final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
rectsClose(initialLocalBounds, clipExpandedBounds);
rectsClose(initialDestinationBounds, clipDestBounds);
rectsClose(initialDestinationBounds, clipExpandedBounds);
canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15), clipOp: ui.ClipOp.difference);
expect(canvas.getLocalClipBounds(), initialLocalBounds);

View File

@@ -123,7 +123,7 @@ Future<void> testMain() async {
canvas.drawImage(atlas, ui.Offset.zero, ui.Paint());
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('ui_atlas.png', region: region);
}, skip: isHtml); // HTML renderer doesn't support drawAtlas
});
test('drawAtlas', () async {
final ui.PictureRecorder recorder = ui.PictureRecorder();
@@ -160,7 +160,7 @@ Future<void> testMain() async {
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('ui_draw_atlas.png', region: region);
}, skip: isHtml); // HTML renderer doesn't support drawAtlas
});
test('drawAtlasRaw', () async {
final ui.PictureRecorder recorder = ui.PictureRecorder();
@@ -209,5 +209,5 @@ Future<void> testMain() async {
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('ui_draw_atlas_raw.png', region: region);
}, skip: isHtml); // HTML renderer doesn't support drawAtlas
});
}

View File

@@ -53,14 +53,14 @@ Future<void> testMain() async {
ui.Paint()..imageFilter = ui.ImageFilter.dilate(radiusX: 5.0, radiusY: 5.0),
);
await matchGoldenFile('ui_filter_dilate_imagefilter.png', region: region);
}, skip: isHtml); // HTML renderer does not support the dilate filter
});
test('erode filter', () async {
await drawTestImageWithPaint(
ui.Paint()..imageFilter = ui.ImageFilter.erode(radiusX: 5.0, radiusY: 5.0),
);
await matchGoldenFile('ui_filter_erode_imagefilter.png', region: region);
}, skip: isHtml); // HTML renderer does not support the erode filter
});
test('matrix filter', () async {
await drawTestImageWithPaint(
@@ -94,7 +94,7 @@ Future<void> testMain() async {
);
await drawTestImageWithPaint(ui.Paint()..imageFilter = filter);
await matchGoldenFile('ui_filter_composed_imagefilters.png', region: region);
}, skip: isHtml); // Only Skwasm and CanvasKit implement composable filters right now.
});
test('compose with colorfilter', () async {
final ui.ImageFilter filter = ui.ImageFilter.compose(
@@ -103,7 +103,7 @@ Future<void> testMain() async {
);
await drawTestImageWithPaint(ui.Paint()..imageFilter = filter);
await matchGoldenFile('ui_filter_composed_colorfilter.png', region: region);
}, skip: isHtml); // Only Skwasm and CanvasKit implements composable filters right now.
});
test('color filter as image filter', () async {
const ui.ColorFilter colorFilter = ui.ColorFilter.mode(
@@ -136,14 +136,14 @@ Future<void> testMain() async {
await drawTestImageWithPaint(ui.Paint()..colorFilter = colorFilter);
await matchGoldenFile('ui_filter_linear_to_srgb_colorfilter.png', region: region);
expect(colorFilter.toString(), 'ColorFilter.linearToSrgbGamma()');
}, skip: isHtml); // HTML renderer hasn't implemented this.
});
test('srgbToLinearGamma color filter', () async {
const ui.ColorFilter colorFilter = ui.ColorFilter.srgbToLinearGamma();
await drawTestImageWithPaint(ui.Paint()..colorFilter = colorFilter);
await matchGoldenFile('ui_filter_srgb_to_linear_colorfilter.png', region: region);
expect(colorFilter.toString(), 'ColorFilter.srgbToLinearGamma()');
}, skip: isHtml); // HTML renderer hasn't implemented this.
});
test('matrix color filter', () async {
const ui.ColorFilter sepia = ui.ColorFilter.matrix(<double>[
@@ -412,62 +412,31 @@ Future<void> testMain() async {
return ui.Rect.fromLTWH(0, 0, offset * columns + pad, offset * rows + pad);
}
test(
'Rendering ops with ImageFilter blur with default tile mode',
() async {
final region = await renderingOpsWithTileMode(null);
await matchGoldenFile(
'ui_filter_blurred_rendering_with_default_tile_mode.png',
region: region,
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
test('Rendering ops with ImageFilter blur with default tile mode', () async {
final region = await renderingOpsWithTileMode(null);
await matchGoldenFile('ui_filter_blurred_rendering_with_default_tile_mode.png', region: region);
});
test(
'Rendering ops with ImageFilter blur with clamp tile mode',
() async {
final region = await renderingOpsWithTileMode(ui.TileMode.clamp);
await matchGoldenFile('ui_filter_blurred_rendering_with_clamp_tile_mode.png', region: region);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
test('Rendering ops with ImageFilter blur with clamp tile mode', () async {
final region = await renderingOpsWithTileMode(ui.TileMode.clamp);
await matchGoldenFile('ui_filter_blurred_rendering_with_clamp_tile_mode.png', region: region);
});
test(
'Rendering ops with ImageFilter blur with decal tile mode',
() async {
final region = await renderingOpsWithTileMode(ui.TileMode.decal);
await matchGoldenFile('ui_filter_blurred_rendering_with_decal_tile_mode.png', region: region);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
test('Rendering ops with ImageFilter blur with decal tile mode', () async {
final region = await renderingOpsWithTileMode(ui.TileMode.decal);
await matchGoldenFile('ui_filter_blurred_rendering_with_decal_tile_mode.png', region: region);
});
test(
'Rendering ops with ImageFilter blur with mirror tile mode',
() async {
final region = await renderingOpsWithTileMode(ui.TileMode.mirror);
await matchGoldenFile(
'ui_filter_blurred_rendering_with_mirror_tile_mode.png',
region: region,
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
test('Rendering ops with ImageFilter blur with mirror tile mode', () async {
final region = await renderingOpsWithTileMode(ui.TileMode.mirror);
await matchGoldenFile('ui_filter_blurred_rendering_with_mirror_tile_mode.png', region: region);
});
test(
'Rendering ops with ImageFilter blur with repeated tile mode',
() async {
final region = await renderingOpsWithTileMode(ui.TileMode.repeated);
await matchGoldenFile(
'ui_filter_blurred_rendering_with_repeated_tile_mode.png',
region: region,
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
test('Rendering ops with ImageFilter blur with repeated tile mode', () async {
final region = await renderingOpsWithTileMode(ui.TileMode.repeated);
await matchGoldenFile(
'ui_filter_blurred_rendering_with_repeated_tile_mode.png',
region: region,
);
});
}

View File

@@ -11,7 +11,6 @@ import 'package:ui/src/engine.dart';
import '../common/fake_asset_manager.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@@ -35,10 +34,7 @@ Future<void> testMain() async {
() async {
final FlutterFontCollection collection = renderer.fontCollection;
final ByteBuffer ahemData = await httpFetchByteBuffer('/assets/fonts/ahem.ttf');
expect(
await collection.loadFontFromList(ahemData.asUint8List()),
!isHtml, // HtmlFontCollection requires family name
);
expect(await collection.loadFontFromList(ahemData.asUint8List()), isTrue);
},
);
@@ -100,13 +96,7 @@ Future<void> testMain() async {
);
expect(result.loadedFonts, <String>[robotoVariableFontUrl, robotoTestFontUrl]);
expect(result.fontFailures, hasLength(1));
if (isHtml) {
// The HTML renderer doesn't have a way to differentiate 404's from other
// download errors.
expect(result.fontFailures[invalidFontUrl], isA<FontDownloadError>());
} else {
expect(result.fontFailures[invalidFontUrl], isA<FontNotFoundError>());
}
expect(result.fontFailures[invalidFontUrl], isA<FontNotFoundError>());
});
test('Loading asset fonts reports when a font has invalid data', () async {
@@ -141,13 +131,7 @@ Future<void> testMain() async {
);
expect(result.loadedFonts, <String>[robotoVariableFontUrl, robotoTestFontUrl]);
expect(result.fontFailures, hasLength(1));
if (isHtml) {
// The HTML renderer doesn't have a way to differentiate invalid data
// from other download errors.
expect(result.fontFailures[invalidFontUrl], isA<FontDownloadError>());
} else {
expect(result.fontFailures[invalidFontUrl], isA<FontInvalidDataError>());
}
expect(result.fontFailures[invalidFontUrl], isA<FontInvalidDataError>());
});
test('Font manifest with numeric and string descriptor values parses correctly', () async {

View File

@@ -96,5 +96,5 @@ Future<void> testMain() async {
// Make sure we can reuse the shader object with a new uniform value and the same Paint object.
shader.setFloat(0, 25.0);
await drawCircleReusePaint('fragment_shader_voronoi_tile25px_reuse_paint.png');
}, skip: isHtml); // Fragment shaders are not supported by the HTML renderer.
});
}

View File

@@ -6,7 +6,6 @@ import 'dart:math' as math;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
@@ -104,5 +103,5 @@ Future<void> testMain() async {
await matchGoldenFile('sweep_gradient_paint.png', region: region);
});
}, skip: isFirefox && isHtml); // https://github.com/flutter/flutter/issues/86623
});
}

View File

@@ -9,7 +9,6 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/canvaskit/image.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/html/image.dart';
import 'package:ui/src/engine/html_image_element_codec.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
@@ -63,12 +62,12 @@ Future<void> testMain() async {
expect(image.height, height);
});
test('loads sample image', () async {
final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png');
final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png');
final ui.FrameInfo frameInfo = await codec.getNextFrame();
expect(codec.imgElement, isNotNull);
expect(codec.imgElement!.src, contains('sample_image1.png'));
expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous');
expect(codec.imgElement!.crossOrigin, 'anonymous');
expect(codec.imgElement!.decoding, 'async');
expect(frameInfo.image, isNotNull);
@@ -76,7 +75,7 @@ Future<void> testMain() async {
expect(frameInfo.image.toString(), '[100×100]');
});
test('dispose image image', () async {
final HtmlImageElementCodec codec = createImageElementCodec('sample_image1.png');
final HtmlImageElementCodec codec = CkImageElementCodec('sample_image1.png');
final ui.FrameInfo frameInfo = await codec.getNextFrame();
expect(frameInfo.image, isNotNull);
expect(frameInfo.image.debugDisposed, isFalse);
@@ -85,7 +84,7 @@ Future<void> testMain() async {
});
test('provides image loading progress', () async {
final StringBuffer buffer = StringBuffer();
final HtmlImageElementCodec codec = createImageElementCodec(
final HtmlImageElementCodec codec = CkImageElementCodec(
'sample_image1.png',
chunkCallback: (int loaded, int total) {
buffer.write('$loaded/$total,');
@@ -98,7 +97,7 @@ Future<void> testMain() async {
/// Regression test for Firefox
/// https://github.com/flutter/flutter/issues/66412
test('Returns nonzero natural width/height', () async {
final HtmlImageElementCodec codec = createImageElementCodec(
final HtmlImageElementCodec codec = CkImageElementCodec(
'data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9I'
'jAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dG'
'l0bGU+QWJzdHJhY3QgaWNvbjwvdGl0bGU+PHBhdGggZD0iTTEyIDBjOS42MDEgMCAx'
@@ -124,7 +123,7 @@ Future<void> testMain() async {
expect(codec.imgElement, isNotNull);
expect(codec.imgElement!.src, contains('sample_image1.png'));
expect(codec.imgElement!.crossOrigin, isHtml ? isNull : 'anonymous');
expect(codec.imgElement!.crossOrigin, 'anonymous');
expect(codec.imgElement!.decoding, 'async');
expect(frameInfo.image, isNotNull);
@@ -146,12 +145,3 @@ Future<void> testMain() async {
});
}, skip: isSkwasm);
}
HtmlImageElementCodec createImageElementCodec(
String src, {
ui_web.ImageCodecChunkCallback? chunkCallback,
}) {
return isHtml
? HtmlRendererImageCodec(src, chunkCallback: chunkCallback)
: CkImageElementCodec(src, chunkCallback: chunkCallback);
}

View File

@@ -230,7 +230,7 @@ Future<void> testMain() async {
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('${name}_fragment_shader_sampler.png', region: drawRegion);
}, skip: isHtml); // HTML doesn't support fragment shaders
});
test('drawVertices with image shader', () async {
final ui.Image image = await generateImage();
@@ -275,7 +275,7 @@ Future<void> testMain() async {
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('${name}_drawVertices_imageShader.png', region: drawRegion);
}, skip: isHtml); // https://github.com/flutter/flutter/issues/127454;
});
test('toByteData_rgba', () async {
final ui.Image image = await generateImage();
@@ -294,7 +294,7 @@ Future<void> testMain() async {
final ByteData? pngData = await image.toByteData(format: ui.ImageByteFormat.png);
expect(pngData, isNotNull);
expect(pngData!.lengthInBytes, isNonZero);
}, skip: isHtml); // https://github.com/flutter/flutter/issues/126611
});
});
}
@@ -347,32 +347,29 @@ Future<void> testMain() async {
return completer.future;
});
// https://github.com/flutter/flutter/issues/126603
if (!isHtml) {
emitImageTests('decodeImageFromPixels_scaled', () {
final Uint8List pixels = generatePixelData(50, 50, (double x, double y) {
final double r = sqrt(x * x + y * y);
final double theta = atan2(x, y);
return ui.Color.fromRGBO(
(255 * (sin(r * 10.0) + 1.0) / 2.0).round(),
(255 * (sin(theta * 10.0) + 1.0) / 2.0).round(),
0,
1,
);
});
final Completer<ui.Image> completer = Completer<ui.Image>();
ui.decodeImageFromPixels(
pixels,
50,
50,
ui.PixelFormat.rgba8888,
completer.complete,
targetWidth: 150,
targetHeight: 150,
emitImageTests('decodeImageFromPixels_scaled', () {
final Uint8List pixels = generatePixelData(50, 50, (double x, double y) {
final double r = sqrt(x * x + y * y);
final double theta = atan2(x, y);
return ui.Color.fromRGBO(
(255 * (sin(r * 10.0) + 1.0) / 2.0).round(),
(255 * (sin(theta * 10.0) + 1.0) / 2.0).round(),
0,
1,
);
return completer.future;
});
}
final Completer<ui.Image> completer = Completer<ui.Image>();
ui.decodeImageFromPixels(
pixels,
50,
50,
ui.PixelFormat.rgba8888,
completer.complete,
targetWidth: 150,
targetHeight: 150,
);
return completer.future;
});
emitImageTests('codec_uri', () async {
final ui.Codec codec = await renderer.instantiateImageCodecFromUrl(
@@ -444,7 +441,7 @@ Future<void> testMain() async {
// This API doesn't work in headless Firefox due to requiring WebGL
// See https://github.com/flutter/flutter/issues/109265
if (!isFirefox && !isHtml) {
if (!isFirefox) {
emitImageTests('svg_image_bitmap_texture_source', () async {
final DomBlob svgBlob = createDomBlob(
<String>[

View File

@@ -8,7 +8,6 @@ import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web/testing.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@@ -26,7 +25,7 @@ Future<void> testMain() async {
expect(paragraph.getLineMetricsAt(0), isNull);
expect(paragraph.numberOfLines, 0);
expect(paragraph.getLineNumberAt(0), isNull);
}, skip: isHtml); // https://github.com/flutter/flutter/issues/144412
});
test('Basic line related metrics', () {
const double fontSize = 10;
@@ -136,7 +135,7 @@ Future<void> testMain() async {
case final List<ui.LineMetrics> metrics:
expect(metrics, hasLength(1));
}
}, skip: isHtml); // The rounding hack doesn't apply to the html renderer
});
test('overrides with flutter test font when debugEmulateFlutterTesterEnvironment is enabled', () {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle());

View File

@@ -7,7 +7,6 @@ import 'package:test/test.dart';
import 'package:ui/ui.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@@ -102,26 +101,18 @@ Future<void> testMain() async {
expect(paragraph.height, lessThan(2 * fontSize));
});
test(
'kTextHeightNone StrutStyle',
() {
const double fontSize = 10;
final ParagraphBuilder builder = ParagraphBuilder(
ParagraphStyle(
fontSize: 100,
fontFamily: 'FlutterTest',
strutStyle: StrutStyle(
forceStrutHeight: true,
height: kTextHeightNone,
fontSize: fontSize,
),
),
);
builder.addText('A');
final Paragraph paragraph = builder.build()..layout(const ParagraphConstraints(width: 1000));
// The height should be much smaller than fontSize * 10.
expect(paragraph.height, lessThan(2 * fontSize));
},
skip: isHtml, // The HTML renderer does not support struts.
);
test('kTextHeightNone StrutStyle', () {
const double fontSize = 10;
final ParagraphBuilder builder = ParagraphBuilder(
ParagraphStyle(
fontSize: 100,
fontFamily: 'FlutterTest',
strutStyle: StrutStyle(forceStrutHeight: true, height: kTextHeightNone, fontSize: fontSize),
),
);
builder.addText('A');
final Paragraph paragraph = builder.build()..layout(const ParagraphConstraints(width: 1000));
// The height should be much smaller than fontSize * 10.
expect(paragraph.height, lessThan(2 * fontSize));
});
}

View File

@@ -9,7 +9,6 @@ import 'package:test/test.dart';
import 'package:ui/ui.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@@ -52,8 +51,7 @@ Future<void> testMain() async {
// the bounds on this will be the same as union - but would draw a missing inside piece.
final Path xor = Path.combine(PathOperation.xor, pathCircle1, pathCircle2);
expect(xor.getBounds(), equals(c1UnionC2));
// TODO(hterkelsen): Implement Path.combine in the HTML renderer, https://github.com/flutter/flutter/issues/44572
}, skip: isHtml);
});
test('path combine oval', () {
final Rect c1 = Rect.fromCircle(center: const Offset(10.0, 10.0), radius: 10.0);
@@ -83,8 +81,7 @@ Future<void> testMain() async {
// the bounds on this will be the same as union - but would draw a missing inside piece.
final Path xor = Path.combine(PathOperation.xor, pathCircle1, pathCircle2);
expect(xor.getBounds(), equals(c1UnionC2));
// TODO(hterkelsen): Implement Path.combine in the HTML renderer, https://github.com/flutter/flutter/issues/44572
}, skip: isHtml);
});
test('path clone', () {
final Path p1 = Path()..lineTo(20.0, 20.0);
@@ -199,8 +196,7 @@ Future<void> testMain() async {
expect(multiContourMetric.iterator.current.length, equals(5.0));
expect(multiContourMetric.iterator.moveNext(), isFalse);
expect(() => multiContourMetric.iterator.current, throwsRangeError);
// TODO(hterkelsen): forceClosed in computeMetrics is ignored in the HTML renderer, https://github.com/flutter/flutter/issues/114446
}, skip: isHtml);
});
test('PathMetrics can remember lengths and isClosed', () {
final Path path =
@@ -219,8 +215,7 @@ Future<void> testMain() async {
expect(metrics[1].isClosed, false);
expect(metrics[1].getTangentForOffset(4.0)!.vector, const Offset(1.0, 0.0));
expect(metrics[1].extractPath(4.0, 6.0).computeMetrics().first.length, 2.0);
// TODO(hterkelsen): isClosed always returns false in the HTML renderer, https://github.com/flutter/flutter/issues/114446
}, skip: isHtml);
});
test('PathMetrics on a mutated path', () {
final Path path = Path()..lineTo(0, 10);
@@ -252,8 +247,7 @@ Future<void> testMain() async {
expect(newFirstMetric.isClosed, true);
expect(newFirstMetric.getTangentForOffset(4.0)!.vector, const Offset(0.0, 1.0));
expect(newFirstMetric.extractPath(4.0, 10.0).computeMetrics().first.length, 6.0);
// TODO(hterkelsen): isClosed always returns false in the HTML renderer, https://github.com/flutter/flutter/issues/114446
}, skip: isHtml);
});
test('path relativeLineTo', () {
final p = Path();

View File

@@ -9,7 +9,6 @@ import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
const ui.Color black = ui.Color(0xFF000000);
const ui.Color red = ui.Color(0xFFFF0000);
@@ -57,7 +56,7 @@ Future<void> testMain() async {
host1.remove();
host2.remove();
host3.remove();
}, skip: isHtml); // HTML renderer doesn't support multi-view.
});
}
DomElement createHostElement(ui.Rect rect) {

View File

@@ -13,7 +13,6 @@ import 'package:web_engine_tester/golden_tester.dart';
import '../common/rendering.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@@ -210,7 +209,7 @@ Future<void> testMain() async {
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_shader_mask.png', region: region);
}, skip: isFirefox && isHtml); // https://github.com/flutter/flutter/issues/86623
});
test('backdrop filter layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
@@ -527,7 +526,7 @@ Future<void> testMain() async {
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_shader_mask_clipped_out.png', region: region);
}, skip: isFirefox && isHtml); // https://github.com/flutter/flutter/issues/86623
});
test('opacity layer with transformed children', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
@@ -597,80 +596,55 @@ Future<void> testMain() async {
);
});
test(
'backdrop layer with default blur tile mode',
() async {
final scene = backdropBlurWithTileMode(null, 10, 50);
await renderScene(scene);
test('backdrop layer with default blur tile mode', () async {
final scene = backdropBlurWithTileMode(null, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_default_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_default_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
});
test(
'backdrop layer with clamp blur tile mode',
() async {
final scene = backdropBlurWithTileMode(ui.TileMode.clamp, 10, 50);
await renderScene(scene);
test('backdrop layer with clamp blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.clamp, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_clamp_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_clamp_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
});
test(
'backdrop layer with mirror blur tile mode',
() async {
final scene = backdropBlurWithTileMode(ui.TileMode.mirror, 10, 50);
await renderScene(scene);
test('backdrop layer with mirror blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.mirror, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_mirror_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_mirror_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
});
test(
'backdrop layer with repeated blur tile mode',
() async {
final scene = backdropBlurWithTileMode(ui.TileMode.repeated, 10, 50);
await renderScene(scene);
test('backdrop layer with repeated blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.repeated, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_repeated_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_repeated_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
});
test(
'backdrop layer with decal blur tile mode',
() async {
final scene = backdropBlurWithTileMode(ui.TileMode.decal, 10, 50);
await renderScene(scene);
test('backdrop layer with decal blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.decal, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_decal_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
},
// HTML renderer doesn't have tile modes
skip: isHtml,
);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_decal_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10 * 50, 10 * 50),
);
});
});
}

View File

@@ -32,9 +32,6 @@ FlutterView get implicitView => EnginePlatformDispatcher.instance.implicitView!;
/// Returns [true] if this test is running in the CanvasKit renderer.
bool get isCanvasKit => renderer is CanvasKitRenderer;
/// Returns [true] if this test is running in the HTML renderer.
bool get isHtml => renderer is HtmlRenderer;
bool get isSkwasm => renderer is SkwasmRenderer;
bool get isMultiThreaded => isSkwasm && (renderer as SkwasmRenderer).isMultiThreaded;

View File

@@ -61,7 +61,7 @@ void testMain() {
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('ui_vertices_antialiased.png', region: region);
}, skip: isHtml); // https://github.com/flutter/flutter/issues/127454
});
}
ui.Vertices _testVertices() {