[web] Fix canvas z-index leaking across repaints when element is reused. (#17378)
* Fix z-index leak. Add test for canvas reuse
* add regression test
* update golden locks
* fix analysis errors
diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml
index f322395..137070e 100644
--- a/lib/web_ui/dev/goldens_lock.yaml
+++ b/lib/web_ui/dev/goldens_lock.yaml
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
-revision: ae6003206eb721137c20cd56d8d1d8e2a76d6dd1
+revision: 5ae87c98ad4abf882a2d312e4c4f75d7ad6d845b
diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart
index 3702513..b97ffed 100644
--- a/lib/web_ui/lib/src/engine/canvas_pool.dart
+++ b/lib/web_ui/lib/src/engine/canvas_pool.dart
@@ -79,6 +79,10 @@
bool requiresClearRect = false;
if (_reusablePool != null && _reusablePool.isNotEmpty) {
_canvas = _reusablePool.removeAt(0);
+ // If a canvas is the first element we set z-index = -1 to workaround
+ // blink compositing bug. To make sure this does not leak when reused
+ // reset z-index.
+ _canvas.style.removeProperty('z-index');
requiresClearRect = true;
} else {
// Compute the final CSS canvas size given the actual pixel count we
diff --git a/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart
new file mode 100644
index 0000000..e92f552
--- /dev/null
+++ b/lib/web_ui/test/golden_tests/engine/canvas_reuse_test.dart
@@ -0,0 +1,98 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.6
+import 'dart:html' as html;
+
+import 'package:ui/ui.dart' hide TextStyle;
+import 'package:ui/src/engine.dart';
+import 'package:test/test.dart';
+
+import 'package:web_engine_tester/golden_tester.dart';
+
+void main() async {
+ const double screenWidth = 600.0;
+ const double screenHeight = 800.0;
+ const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
+ final Paint testPaint = Paint()
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 2.0
+ ..color = const Color(0xFFFF00FF);
+
+ setUp(() async {
+ debugEmulateFlutterTesterEnvironment = true;
+ await webOnlyInitializePlatform();
+ webOnlyFontCollection.debugRegisterTestFonts();
+ await webOnlyFontCollection.ensureFontsLoaded();
+ });
+
+ // 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);
+ 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.apply(engineCanvas);
+ engineCanvas.endOfPaint();
+
+ html.Element sceneElement = html.Element.tag('flt-scene');
+ sceneElement.append(engineCanvas.rootElement);
+ html.document.body.append(sceneElement);
+
+ final html.CanvasElement canvas = html.document.querySelector('canvas');
+ // ! 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(0, 0), Paint());
+ rc2.drawPath(path2, testPaint);
+ rc2.apply(engineCanvas);
+
+ sceneElement = html.Element.tag('flt-scene');
+ sceneElement.append(engineCanvas.rootElement);
+ html.document.body.append(sceneElement);
+
+ final html.CanvasElement canvas2 = html.document.querySelector('canvas');
+ // ZIndex should have been cleared since we have image element preceding
+ // canvas.
+ expect(canvas.style.zIndex != '-1', true);
+ 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(
+ html.ImageElement()
+ ..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage',
+ 20,
+ 20,
+ );
+}