| // 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:js/js_util.dart' 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.dart'; |
| import 'test_data.dart'; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| void testMain() { |
| group('$HtmlViewEmbedder', () { |
| setUpCanvasKitTest(); |
| |
| setUp(() { |
| window.debugOverrideDevicePixelRatio(1); |
| }); |
| |
| test('embeds interactive platform views', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| rasterizer.draw(sb.build().layerTree); |
| |
| // The platform view is now split in two parts. The contents live |
| // as a child of the glassPane, and the slot lives in the glassPane |
| // shadow root. The slot is the one that has pointer events auto. |
| final DomElement contents = flutterViewEmbedder.glassPaneElement |
| .querySelector('#view-0')!; |
| final DomElement slot = flutterViewEmbedder.sceneElement! |
| .querySelector('slot')!; |
| final DomElement contentsHost = contents.parent!; |
| final DomElement slotHost = slot.parent!; |
| |
| expect(contents, isNotNull, |
| reason: 'The view from the factory is injected in the DOM.'); |
| |
| expect(contentsHost.tagName, equalsIgnoringCase('flt-platform-view')); |
| expect(slotHost.tagName, equalsIgnoringCase('flt-platform-view-slot')); |
| |
| expect(slotHost.style.pointerEvents, 'auto', |
| reason: 'The slot reenables pointer events.'); |
| expect(contentsHost.getAttribute('slot'), slot.getAttribute('name'), |
| reason: 'The contents and slot are correctly related.'); |
| }); |
| |
| test('clips platform views with RRects', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.pushClipRRect( |
| ui.RRect.fromLTRBR(0, 0, 10, 10, const ui.Radius.circular(3))); |
| sb.addPlatformView(0, width: 10, height: 10); |
| rasterizer.draw(sb.build().layerTree); |
| |
| expect( |
| flutterViewEmbedder.sceneElement! |
| .querySelectorAll('#sk_path_defs') |
| .single, |
| isNotNull, |
| ); |
| expect( |
| flutterViewEmbedder.sceneElement! |
| .querySelectorAll('#sk_path_defs') |
| .single |
| .querySelectorAll('clipPath') |
| .single, |
| isNotNull, |
| ); |
| expect( |
| flutterViewEmbedder.sceneElement! |
| .querySelectorAll('flt-clip') |
| .single |
| .style |
| .clipPath, |
| 'url("#svgClip1")', |
| ); |
| expect( |
| flutterViewEmbedder.sceneElement! |
| .querySelectorAll('flt-clip') |
| .single |
| .style |
| .width, |
| '100%', |
| ); |
| expect( |
| flutterViewEmbedder.sceneElement! |
| .querySelectorAll('flt-clip') |
| .single |
| .style |
| .height, |
| '100%', |
| ); |
| }); |
| |
| test('correctly transforms platform views', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| final Matrix4 scaleMatrix = Matrix4.identity() |
| ..scale(5, 5) |
| ..translate(100, 100); |
| sb.pushTransform(scaleMatrix.toFloat64()); |
| sb.pushOffset(3, 3); |
| sb.addPlatformView(0, width: 10, height: 10); |
| rasterizer.draw(sb.build().layerTree); |
| |
| // Transformations happen on the slot element. |
| final DomElement slotHost = flutterViewEmbedder.sceneElement! |
| .querySelector('flt-platform-view-slot')!; |
| |
| expect( |
| slotHost.style.transform, |
| // We should apply the scale matrix first, then the offset matrix. |
| // So the translate should be 515 (5 * 100 + 5 * 3), and not |
| // 503 (5 * 100 + 3). |
| 'matrix3d(5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 515, 515, 0, 1)', |
| ); |
| }); |
| |
| test('correctly offsets platform views', () async { |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.addPlatformView(0, offset: const ui.Offset(3, 4), width: 5, height: 6); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| |
| final DomElement slotHost = flutterViewEmbedder.sceneElement! |
| .querySelector('flt-platform-view-slot')!; |
| final DomCSSStyleDeclaration style = slotHost.style; |
| |
| expect(style.transform, 'matrix(1, 0, 0, 1, 3, 4)'); |
| expect(style.width, '5px'); |
| expect(style.height, '6px'); |
| |
| final DomRect slotRect = slotHost.getBoundingClientRect(); |
| expect(slotRect.left, 3); |
| expect(slotRect.top, 4); |
| expect(slotRect.right, 8); |
| expect(slotRect.bottom, 10); |
| }); |
| |
| // Returns the list of CSS transforms applied to the ancestor chain of |
| // elements starting from `viewHost`, up until and excluding <flt-scene>. |
| List<String> getTransformChain(DomElement viewHost) { |
| final List<String> chain = <String>[]; |
| DomElement? element = viewHost; |
| while (element != null && element.tagName.toLowerCase() != 'flt-scene') { |
| chain.add(element.style.transform); |
| element = element.parent; |
| } |
| return chain; |
| } |
| |
| test('correctly offsets when clip chain length is changed', () async { |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(3, 3); |
| sb.pushClipRect(ui.Rect.largest); |
| sb.pushOffset(6, 6); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.pop(); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| |
| // Transformations happen on the slot element. |
| DomElement slotHost = flutterViewEmbedder.sceneElement! |
| .querySelector('flt-platform-view-slot')!; |
| |
| expect( |
| getTransformChain(slotHost), |
| <String>[ |
| 'matrix(1, 0, 0, 1, 6, 6)', |
| 'matrix(1, 0, 0, 1, 3, 3)', |
| ], |
| ); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(3, 3); |
| sb.pushClipRect(ui.Rect.largest); |
| sb.pushOffset(6, 6); |
| sb.pushClipRect(ui.Rect.largest); |
| sb.pushOffset(9, 9); |
| sb.addPlatformView(0, width: 10, height: 10); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| |
| // Transformations happen on the slot element. |
| slotHost = flutterViewEmbedder.sceneElement! |
| .querySelector('flt-platform-view-slot')!; |
| |
| expect( |
| getTransformChain(slotHost), |
| <String>[ |
| 'matrix(1, 0, 0, 1, 9, 9)', |
| 'matrix(1, 0, 0, 1, 6, 6)', |
| 'matrix(1, 0, 0, 1, 3, 3)', |
| ], |
| ); |
| }); |
| |
| test('converts device pixels to logical pixels (no clips)', () async { |
| window.debugOverrideDevicePixelRatio(4); |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(1, 1); |
| sb.pushOffset(2, 2); |
| sb.pushOffset(3, 3); |
| sb.addPlatformView(0, width: 10, height: 10); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| |
| // Transformations happen on the slot element. |
| final DomElement slotHost = flutterViewEmbedder.sceneElement! |
| .querySelector('flt-platform-view-slot')!; |
| |
| expect( |
| getTransformChain(slotHost), |
| <String>['matrix(0.25, 0, 0, 0.25, 1.5, 1.5)'], |
| ); |
| }); |
| |
| test('converts device pixels to logical pixels (with clips)', () async { |
| window.debugOverrideDevicePixelRatio(4); |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(3, 3); |
| sb.pushClipRect(ui.Rect.largest); |
| sb.pushOffset(6, 6); |
| sb.pushClipRect(ui.Rect.largest); |
| sb.pushOffset(9, 9); |
| sb.addPlatformView(0, width: 10, height: 10); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| |
| // Transformations happen on the slot element. |
| final DomElement slotHost = flutterViewEmbedder.sceneElement! |
| .querySelector('flt-platform-view-slot')!; |
| |
| expect( |
| getTransformChain(slotHost), |
| <String>[ |
| 'matrix(1, 0, 0, 1, 9, 9)', |
| 'matrix(1, 0, 0, 1, 6, 6)', |
| 'matrix(0.25, 0, 0, 0.25, 0.75, 0.75)', |
| ], |
| ); |
| }); |
| |
| test('renders overlays on top of platform views', () async { |
| expect(SurfaceFactory.instance.debugCacheSize, 0); |
| expect(configuration.canvasKitMaximumSurfaces, 8); |
| final CkPicture testPicture = |
| paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) { |
| canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint()); |
| }); |
| |
| // Initialize all platform views to be used in the test. |
| final List<int> platformViewIds = <int>[]; |
| for (int i = 0; i < 16; i++) { |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-$i', |
| ); |
| await createPlatformView(i, 'test-platform-view'); |
| platformViewIds.add(i); |
| } |
| |
| void renderTestScene({required int viewCount}) { |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| for (int i = 0; i < viewCount; i++) { |
| sb.addPicture(ui.Offset.zero, testPicture); |
| sb.addPlatformView(i, width: 10, height: 10); |
| } |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| } |
| |
| // Frame 1: |
| // Render: up to cache size platform views. |
| // Expect: main canvas plus platform view overlays. |
| renderTestScene(viewCount: 8); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| ]); |
| |
| // Frame 2: |
| // Render: zero platform views. |
| // Expect: main canvas, no overlays. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(viewCount: 0); |
| _expectSceneMatches(<_EmbeddedViewMarker>[_overlay]); |
| |
| // Frame 3: |
| // Render: less than cache size platform views. |
| // Expect: overlays reused. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(viewCount: 6); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| // Frame 4: |
| // Render: more platform views than max cache size. |
| // Expect: main canvas, backup overlay, maximum overlays. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(viewCount: 16); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| ]); |
| |
| // Frame 5: |
| // Render: zero platform views. |
| // Expect: main canvas, no overlays. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(viewCount: 0); |
| _expectSceneMatches(<_EmbeddedViewMarker>[_overlay]); |
| |
| // Frame 6: |
| // Render: deleted platform views. |
| // Expect: error. |
| for (final int id in platformViewIds) { |
| const StandardMethodCodec codec = StandardMethodCodec(); |
| final Completer<void> completer = Completer<void>(); |
| ui.window.sendPlatformMessage( |
| 'flutter/platform_views', |
| codec.encodeMethodCall(MethodCall( |
| 'dispose', |
| id, |
| )), |
| completer.complete, |
| ); |
| await completer.future; |
| } |
| |
| try { |
| renderTestScene(viewCount: platformViewIds.length); |
| fail('Expected to throw'); |
| } on AssertionError catch (error) { |
| expect( |
| error.toString(), |
| 'Assertion failed: "Cannot render platform views: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15. These views have not been created, or they have been deleted."', |
| ); |
| } |
| |
| // Frame 7: |
| // Render: a platform view after error. |
| // Expect: success. Just checking the system is not left in a corrupted state. |
| await createPlatformView(0, 'test-platform-view'); |
| renderTestScene(viewCount: 0); |
| _expectSceneMatches(<_EmbeddedViewMarker>[_overlay]); |
| // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 |
| }, skip: isSafari); |
| |
| test('correctly reuses overlays', () async { |
| final CkPicture testPicture = |
| paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) { |
| canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint()); |
| }); |
| |
| // Initialize all platform views to be used in the test. |
| final List<int> platformViewIds = <int>[]; |
| for (int i = 0; i < 20; i++) { |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-$i', |
| ); |
| await createPlatformView(i, 'test-platform-view'); |
| platformViewIds.add(i); |
| } |
| |
| void renderTestScene(List<int> views) { |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| for (final int view in views) { |
| sb.addPicture(ui.Offset.zero, testPicture); |
| sb.addPlatformView(view, width: 10, height: 10); |
| } |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| } |
| |
| // Frame 1: |
| // Render: Views 1-10 |
| // Expect: main canvas plus platform view overlays; empty cache. |
| renderTestScene(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); |
| expect(SurfaceFactory.instance.numAvailableOverlays, 0); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| ]); |
| |
| // Frame 2: |
| // Render: Views 2-11 |
| // Expect: main canvas plus platform view overlays; empty cache. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(<int>[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); |
| expect(SurfaceFactory.instance.numAvailableOverlays, 0); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| ]); |
| |
| // Frame 3: |
| // Render: Views 3-12 |
| // Expect: main canvas plus platform view overlays; empty cache. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(<int>[3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| ]); |
| |
| // Frame 4: |
| // Render: Views 3-12 again (same as last frame) |
| // Expect: main canvas plus platform view overlays; empty cache. |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(<int>[3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| ]); |
| |
| // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 |
| }, skip: isSafari); |
| |
| test('embeds and disposes of a platform view', () async { |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| expect( |
| flutterViewEmbedder.glassPaneElement |
| .querySelector('flt-platform-view'), |
| isNotNull, |
| ); |
| |
| await disposePlatformView(0); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| ]); |
| |
| expect( |
| flutterViewEmbedder.glassPaneElement |
| .querySelector('flt-platform-view'), |
| isNull, |
| ); |
| }); |
| |
| test('does not crash when resizing the window after textures have been registered', () async { |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final CkBrowserImageDecoder image = await CkBrowserImageDecoder.create( |
| data: kAnimatedGif, |
| debugSource: 'test', |
| ); |
| final ui.FrameInfo frame = await image.getNextFrame(); |
| final CkImage ckImage = frame.image as CkImage; |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| final CkPictureRecorder recorder = CkPictureRecorder(); |
| final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest); |
| canvas.drawImage(ckImage, ui.Offset.zero, CkPaint()); |
| final CkPicture picture = recorder.endRecording(); |
| sb.addPicture(ui.Offset.zero, picture); |
| sb.addPlatformView(0, width: 10, height: 10); |
| |
| window.webOnlyDebugPhysicalSizeOverride = const ui.Size(100, 100); |
| window.debugForceResize(); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| window.webOnlyDebugPhysicalSizeOverride = const ui.Size(200, 200); |
| window.debugForceResize(); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| window.webOnlyDebugPhysicalSizeOverride = null; |
| window.debugForceResize(); |
| // ImageDecoder is not supported in Safari or Firefox. |
| }, skip: isSafari || isFirefox); |
| |
| test('removed the DOM node of an unrendered platform view', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| expect( |
| flutterViewEmbedder.glassPaneElement |
| .querySelector('flt-platform-view'), |
| isNotNull, |
| ); |
| |
| // Render a frame with a different platform view. |
| await createPlatformView(1, 'test-platform-view'); |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(1, width: 10, height: 10); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| expect( |
| flutterViewEmbedder.glassPaneElement |
| .querySelectorAll('flt-platform-view'), |
| hasLength(2)); |
| |
| // Render a frame without a platform view, but also without disposing of |
| // the platform view. |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| ]); |
| |
| // The actual contents of the platform view are kept in the dom, until |
| // it's actually disposed of! |
| expect( |
| flutterViewEmbedder.glassPaneElement |
| .querySelectorAll('flt-platform-view'), |
| hasLength(2)); |
| }); |
| |
| test( |
| 'removes old SVG clip definitions from the DOM when the view is recomposited', |
| () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'test-view', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| void renderTestScene() { |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.pushClipRRect( |
| ui.RRect.fromLTRBR(0, 0, 10, 10, const ui.Radius.circular(3))); |
| sb.addPlatformView(0, width: 10, height: 10); |
| rasterizer.draw(sb.build().layerTree); |
| } |
| |
| final DomNode skPathDefs = flutterViewEmbedder.sceneElement! |
| .querySelector('#sk_path_defs')!; |
| |
| expect(skPathDefs.childNodes, hasLength(0)); |
| |
| renderTestScene(); |
| expect(skPathDefs.childNodes, hasLength(1)); |
| |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(); |
| expect(skPathDefs.childNodes, hasLength(1)); |
| |
| await Future<void>.delayed(Duration.zero); |
| renderTestScene(); |
| expect(skPathDefs.childNodes, hasLength(1)); |
| }); |
| |
| test('does not crash when a prerolled platform view is not composited', |
| () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.pushClipRect(ui.Rect.zero); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.pop(); |
| // The below line should not throw an error. |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| ]); |
| }); |
| |
| test('does not crash when overlays are disabled', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| HtmlViewEmbedder.debugDisableOverlays = true; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| |
| final LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.pop(); |
| // The below line should not throw an error. |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| ]); |
| HtmlViewEmbedder.debugDisableOverlays = false; |
| }); |
| |
| test('works correctly with max overlays == 2', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| final FlutterConfiguration config = FlutterConfiguration() |
| ..setUserConfiguration( |
| js_util.jsify(<String, Object?>{ |
| 'canvasKitMaximumSurfaces': 2, |
| }) as JsFlutterConfiguration); |
| debugSetConfiguration(config); |
| |
| SurfaceFactory.instance.debugClear(); |
| |
| expect(SurfaceFactory.instance.maximumSurfaces, 2); |
| expect(SurfaceFactory.instance.maximumOverlays, 1); |
| |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| await createPlatformView(1, 'test-platform-view'); |
| |
| LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.pop(); |
| // The below line should not throw an error. |
| rasterizer.draw(sb.build().layerTree); |
| |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| ]); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.pop(); |
| // The below line should not throw an error. |
| rasterizer.draw(sb.build().layerTree); |
| |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _overlay, |
| _platformView, |
| ]); |
| |
| // Reset configuration |
| debugSetConfiguration(FlutterConfiguration()); |
| }); |
| |
| test( |
| 'correctly renders when overlays are disabled and a subset ' |
| 'of views is used', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| HtmlViewEmbedder.debugDisableOverlays = true; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-platform-view', |
| (int viewId) => createDomHTMLDivElement()..id = 'view-0', |
| ); |
| await createPlatformView(0, 'test-platform-view'); |
| await createPlatformView(1, 'test-platform-view'); |
| |
| LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.pop(); |
| // The below line should not throw an error. |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| ]); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.pop(); |
| // The below line should not throw an error. |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| ]); |
| |
| HtmlViewEmbedder.debugDisableOverlays = false; |
| }); |
| |
| test('does not create overlays for invisible platform views', () async { |
| final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-visible-view', |
| (int viewId) => |
| createDomHTMLDivElement()..className = 'visible-platform-view'); |
| ui.platformViewRegistry.registerViewFactory( |
| 'test-invisible-view', |
| (int viewId) => |
| createDomHTMLDivElement()..className = 'invisible-platform-view', |
| isVisible: false, |
| ); |
| await createPlatformView(0, 'test-visible-view'); |
| await createPlatformView(1, 'test-invisible-view'); |
| await createPlatformView(2, 'test-visible-view'); |
| await createPlatformView(3, 'test-invisible-view'); |
| await createPlatformView(4, 'test-invisible-view'); |
| await createPlatformView(5, 'test-invisible-view'); |
| await createPlatformView(6, 'test-invisible-view'); |
| |
| expect(platformViewManager.isInvisible(0), isFalse); |
| expect(platformViewManager.isInvisible(1), isTrue); |
| |
| LayerSceneBuilder sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| ], reason: 'Invisible view alone renders on top of base overlay.'); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| ], reason: 'Overlay created after a group containing a visible view.'); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| _platformView, |
| _overlay, |
| ], reason: 'Overlays created after each group containing a visible view.'); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| ], reason: 'Invisible views grouped in with visible views.'); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.addPlatformView(4, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _overlay, |
| ]); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.addPlatformView(4, width: 10, height: 10); |
| sb.addPlatformView(5, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _overlay, |
| ]); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(0, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.addPlatformView(4, width: 10, height: 10); |
| sb.addPlatformView(5, width: 10, height: 10); |
| sb.addPlatformView(6, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _overlay, |
| ]); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.addPlatformView(4, width: 10, height: 10); |
| sb.addPlatformView(5, width: 10, height: 10); |
| sb.addPlatformView(6, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| ], reason: 'Many invisible views can be rendered on top of the base overlay.'); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.addPlatformView(4, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _overlay, |
| ]); |
| |
| sb = LayerSceneBuilder(); |
| sb.pushOffset(0, 0); |
| sb.addPlatformView(4, width: 10, height: 10); |
| sb.addPlatformView(3, width: 10, height: 10); |
| sb.addPlatformView(2, width: 10, height: 10); |
| sb.addPlatformView(1, width: 10, height: 10); |
| sb.pop(); |
| rasterizer.draw(sb.build().layerTree); |
| _expectSceneMatches(<_EmbeddedViewMarker>[ |
| _overlay, |
| _platformView, |
| _platformView, |
| _platformView, |
| _platformView, |
| _overlay, |
| ]); |
| }); |
| }); |
| } |
| |
| // Used to test that the platform views and overlays are in the correct order in |
| // the scene. |
| enum _EmbeddedViewMarker { |
| overlay, |
| platformView, |
| } |
| |
| _EmbeddedViewMarker get _overlay => _EmbeddedViewMarker.overlay; |
| _EmbeddedViewMarker get _platformView => _EmbeddedViewMarker.platformView; |
| |
| const Map<String, _EmbeddedViewMarker> _tagToViewMarker = <String, _EmbeddedViewMarker>{ |
| 'flt-canvas-container': _EmbeddedViewMarker.overlay, |
| 'flt-platform-view-slot': _EmbeddedViewMarker.platformView, |
| }; |
| |
| void _expectSceneMatches(List<_EmbeddedViewMarker> expectedMarkers, { |
| String? reason, |
| }) { |
| // Convert the scene elements to its corresponding array of _EmbeddedViewMarker |
| final List<_EmbeddedViewMarker> sceneElements = flutterViewEmbedder |
| .sceneElement!.children |
| .where((DomElement element) => element.tagName != 'svg') |
| .map((DomElement element) => _tagToViewMarker[element.tagName.toLowerCase()]!) |
| .toList(); |
| |
| expect(sceneElements, expectedMarkers, reason: reason); |
| } |