// Copyright 2017 The Chromium 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:convert'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import '../flutter_test_alternative.dart'; class TestServiceExtensionsBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { final Map extensions = {}; final Map>> eventsDispatched = >>{}; @override void registerServiceExtension({ @required String name, @required ServiceExtensionCallback callback }) { expect(extensions.containsKey(name), isFalse); extensions[name] = callback; } @override void postEvent(String eventKind, Map eventData) { getEventsDispatched(eventKind).add(eventData); } List> getEventsDispatched(String eventKind) { return eventsDispatched.putIfAbsent(eventKind, () => >[]); } Iterable> getServiceExtensionStateChangedEvents(String extensionName) { return getEventsDispatched('Flutter.ServiceExtensionStateChanged') .where((Map event) => event['extension'] == extensionName); } Future> testExtension(String name, Map arguments) { expect(extensions.containsKey(name), isTrue); return extensions[name](arguments); } int reassembled = 0; bool pendingReassemble = false; @override Future performReassemble() { reassembled += 1; pendingReassemble = true; return super.performReassemble(); } bool frameScheduled = false; @override void scheduleFrame() { frameScheduled = true; } Future doFrame() async { frameScheduled = false; if (ui.window.onBeginFrame != null) ui.window.onBeginFrame(Duration.zero); await flushMicrotasks(); if (ui.window.onDrawFrame != null) ui.window.onDrawFrame(); } @override void scheduleForcedFrame() { expect(true, isFalse); } @override void scheduleWarmUpFrame() { expect(pendingReassemble, isTrue); pendingReassemble = false; } Future flushMicrotasks() { final Completer completer = Completer(); Timer.run(completer.complete); return completer.future; } } TestServiceExtensionsBinding binding; Future> hasReassemble(Future> pendingResult) async { bool completed = false; pendingResult.whenComplete(() { completed = true; }); expect(binding.frameScheduled, isFalse); await binding.flushMicrotasks(); expect(binding.frameScheduled, isTrue); expect(completed, isFalse); await binding.flushMicrotasks(); await binding.doFrame(); await binding.flushMicrotasks(); expect(completed, isTrue); expect(binding.frameScheduled, isFalse); return pendingResult; } void main() { final List console = []; test('Service extensions - pretest', () async { binding = TestServiceExtensionsBinding(); expect(binding.frameScheduled, isTrue); // We need to test this service extension here because the result is true // after the first binding.doFrame() call. Map firstFrameResult; expect(binding.debugDidSendFirstFrameEvent, isFalse); firstFrameResult = await binding.testExtension('didSendFirstFrameEvent', {}); expect(firstFrameResult, { 'enabled': 'false' }); await binding.doFrame(); // initial frame scheduled by creating the binding expect(binding.debugDidSendFirstFrameEvent, isTrue); firstFrameResult = await binding.testExtension('didSendFirstFrameEvent', {}); expect(firstFrameResult, { 'enabled': 'true' }); expect(binding.frameScheduled, isFalse); expect(debugPrint, equals(debugPrintThrottled)); debugPrint = (String message, { int wrapWidth }) { console.add(message); }; }); // The following list is alphabetical, one test per extension. // // The order doesn't really matter except that the pretest and posttest tests // must be first and last respectively. test('Service extensions - debugAllowBanner', () async { Map result; expect(binding.frameScheduled, isFalse); expect(WidgetsApp.debugAllowBannerOverride, true); result = await binding.testExtension('debugAllowBanner', {}); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.debugAllowBannerOverride, true); result = await binding.testExtension('debugAllowBanner', { 'enabled': 'false' }); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.debugAllowBannerOverride, false); result = await binding.testExtension('debugAllowBanner', {}); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.debugAllowBannerOverride, false); result = await binding.testExtension('debugAllowBanner', { 'enabled': 'true' }); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.debugAllowBannerOverride, true); result = await binding.testExtension('debugAllowBanner', {}); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.debugAllowBannerOverride, true); expect(binding.frameScheduled, isFalse); }); test('Service extensions - debugDumpApp', () async { Map result; result = await binding.testExtension('debugDumpApp', {}); expect(result, {}); expect(console, ['TestServiceExtensionsBinding - CHECKED MODE', '']); console.clear(); }); test('Service extensions - debugDumpRenderTree', () async { Map result; await binding.doFrame(); result = await binding.testExtension('debugDumpRenderTree', {}); expect(result, {}); expect(console, [ matches( r'^' r'RenderView#[0-9a-f]{5}\n' r' debug mode enabled - [a-zA-Z]+\n' r' window size: Size\(2400\.0, 1800\.0\) \(in physical pixels\)\n' r' device pixel ratio: 3\.0 \(physical pixels per logical pixel\)\n' r' configuration: Size\(800\.0, 600\.0\) at 3\.0x \(in logical pixels\)\n' r'$' ), ]); console.clear(); }); test('Service extensions - debugDumpLayerTree', () async { Map result; await binding.doFrame(); result = await binding.testExtension('debugDumpLayerTree', {}); expect(result, {}); expect(console, [ matches( r'^' r'TransformLayer#[0-9a-f]{5}\n' r' owner: RenderView#[0-9a-f]{5}\n' r' creator: RenderView\n' r' offset: Offset\(0\.0, 0\.0\)\n' r' transform:\n' r' \[0] 3\.0,0\.0,0\.0,0\.0\n' r' \[1] 0\.0,3\.0,0\.0,0\.0\n' r' \[2] 0\.0,0\.0,1\.0,0\.0\n' r' \[3] 0\.0,0\.0,0\.0,1\.0\n' r'$' ), ]); console.clear(); }); test('Service extensions - debugDumpSemanticsTreeInTraversalOrder', () async { Map result; await binding.doFrame(); result = await binding.testExtension('debugDumpSemanticsTreeInTraversalOrder', {}); expect(result, {}); expect(console, ['Semantics not collected.']); console.clear(); }); test('Service extensions - debugDumpSemanticsTreeInInverseHitTestOrder', () async { Map result; await binding.doFrame(); result = await binding.testExtension('debugDumpSemanticsTreeInInverseHitTestOrder', {}); expect(result, {}); expect(console, ['Semantics not collected.']); console.clear(); }); test('Service extensions - debugPaint', () async { final Iterable> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.debugPaint'); Map extensionChangedEvent; Map result; Future> pendingResult; bool completed; expect(binding.frameScheduled, isFalse); expect(debugPaintSizeEnabled, false); result = await binding.testExtension('debugPaint', {}); expect(result, { 'enabled': 'false' }); expect(debugPaintSizeEnabled, false); expect(extensionChangedEvents, isEmpty); expect(binding.frameScheduled, isFalse); pendingResult = binding.testExtension('debugPaint', { 'enabled': 'true' }); completed = false; pendingResult.whenComplete(() { completed = true; }); await binding.flushMicrotasks(); expect(binding.frameScheduled, isTrue); expect(completed, isFalse); await binding.doFrame(); await binding.flushMicrotasks(); expect(completed, isTrue); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, { 'enabled': 'true' }); expect(debugPaintSizeEnabled, true); expect(extensionChangedEvents.length, 1); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.debugPaint'); expect(extensionChangedEvent['value'], 'true'); result = await binding.testExtension('debugPaint', {}); expect(result, { 'enabled': 'true' }); expect(debugPaintSizeEnabled, true); expect(extensionChangedEvents.length, 1); expect(binding.frameScheduled, isFalse); pendingResult = binding.testExtension('debugPaint', { 'enabled': 'false' }); await binding.flushMicrotasks(); expect(binding.frameScheduled, isTrue); await binding.doFrame(); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, { 'enabled': 'false' }); expect(debugPaintSizeEnabled, false); expect(extensionChangedEvents.length, 2); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.debugPaint'); expect(extensionChangedEvent['value'], 'false'); result = await binding.testExtension('debugPaint', {}); expect(result, { 'enabled': 'false' }); expect(debugPaintSizeEnabled, false); expect(extensionChangedEvents.length, 2); expect(binding.frameScheduled, isFalse); }); test('Service extensions - debugPaintBaselinesEnabled', () async { Map result; Future> pendingResult; bool completed; expect(binding.frameScheduled, isFalse); expect(debugPaintBaselinesEnabled, false); result = await binding.testExtension('debugPaintBaselinesEnabled', {}); expect(result, { 'enabled': 'false' }); expect(debugPaintBaselinesEnabled, false); expect(binding.frameScheduled, isFalse); pendingResult = binding.testExtension('debugPaintBaselinesEnabled', { 'enabled': 'true' }); completed = false; pendingResult.whenComplete(() { completed = true; }); await binding.flushMicrotasks(); expect(binding.frameScheduled, isTrue); expect(completed, isFalse); await binding.doFrame(); await binding.flushMicrotasks(); expect(completed, isTrue); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, { 'enabled': 'true' }); expect(debugPaintBaselinesEnabled, true); result = await binding.testExtension('debugPaintBaselinesEnabled', {}); expect(result, { 'enabled': 'true' }); expect(debugPaintBaselinesEnabled, true); expect(binding.frameScheduled, isFalse); pendingResult = binding.testExtension('debugPaintBaselinesEnabled', { 'enabled': 'false' }); await binding.flushMicrotasks(); expect(binding.frameScheduled, isTrue); await binding.doFrame(); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, { 'enabled': 'false' }); expect(debugPaintBaselinesEnabled, false); result = await binding.testExtension('debugPaintBaselinesEnabled', {}); expect(result, { 'enabled': 'false' }); expect(debugPaintBaselinesEnabled, false); expect(binding.frameScheduled, isFalse); }); test('Service extensions - profileWidgetBuilds', () async { Map result; expect(binding.frameScheduled, isFalse); expect(debugProfileBuildsEnabled, false); result = await binding.testExtension('profileWidgetBuilds', {}); expect(result, { 'enabled': 'false' }); expect(debugProfileBuildsEnabled, false); result = await binding.testExtension('profileWidgetBuilds', { 'enabled': 'true' }); expect(result, { 'enabled': 'true' }); expect(debugProfileBuildsEnabled, true); result = await binding.testExtension('profileWidgetBuilds', {}); expect(result, { 'enabled': 'true' }); expect(debugProfileBuildsEnabled, true); result = await binding.testExtension('profileWidgetBuilds', { 'enabled': 'false' }); expect(result, { 'enabled': 'false' }); expect(debugProfileBuildsEnabled, false); result = await binding.testExtension('profileWidgetBuilds', {}); expect(result, { 'enabled': 'false' }); expect(debugProfileBuildsEnabled, false); expect(binding.frameScheduled, isFalse); }); test('Service extensions - evict', () async { Map result; bool completed; completed = false; BinaryMessages.setMockMessageHandler('flutter/assets', (ByteData message) async { expect(utf8.decode(message.buffer.asUint8List()), 'test'); completed = true; return ByteData(5); // 0x0000000000 }); bool data; data = await rootBundle.loadStructuredData('test', (String value) async { expect(value, '\x00\x00\x00\x00\x00'); return true; }); expect(data, isTrue); expect(completed, isTrue); completed = false; data = await rootBundle.loadStructuredData('test', (String value) async { expect(true, isFalse); return null; }); expect(data, isTrue); expect(completed, isFalse); result = await binding.testExtension('evict', { 'value': 'test' }); expect(result, { 'value': '' }); expect(completed, isFalse); data = await rootBundle.loadStructuredData('test', (String value) async { expect(value, '\x00\x00\x00\x00\x00'); return false; }); expect(data, isFalse); expect(completed, isTrue); BinaryMessages.setMockMessageHandler('flutter/assets', null); }); test('Service extensions - exit', () async { // no test for _calling_ 'exit', because that should terminate the process! expect(binding.extensions.containsKey('exit'), isTrue); }); test('Service extensions - platformOverride', () async { final Iterable> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.platformOverride'); Map extensionChangedEvent; Map result; expect(binding.reassembled, 0); expect(defaultTargetPlatform, TargetPlatform.android); result = await binding.testExtension('platformOverride', {}); expect(result, {'value': 'android'}); expect(defaultTargetPlatform, TargetPlatform.android); expect(extensionChangedEvents, isEmpty); result = await hasReassemble(binding.testExtension('platformOverride', {'value': 'iOS'})); expect(result, {'value': 'iOS'}); expect(binding.reassembled, 1); expect(defaultTargetPlatform, TargetPlatform.iOS); expect(extensionChangedEvents.length, 1); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'iOS'); result = await hasReassemble(binding.testExtension('platformOverride', {'value': 'android'})); expect(result, {'value': 'android'}); expect(binding.reassembled, 2); expect(defaultTargetPlatform, TargetPlatform.android); expect(extensionChangedEvents.length, 2); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'android'); result = await hasReassemble(binding.testExtension('platformOverride', {'value': 'fuchsia'})); expect(result, {'value': 'fuchsia'}); expect(binding.reassembled, 3); expect(defaultTargetPlatform, TargetPlatform.fuchsia); expect(extensionChangedEvents.length, 3); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'fuchsia'); result = await hasReassemble(binding.testExtension('platformOverride', {'value': 'default'})); expect(result, {'value': 'android'}); expect(binding.reassembled, 4); expect(defaultTargetPlatform, TargetPlatform.android); expect(extensionChangedEvents.length, 4); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'android'); result = await hasReassemble(binding.testExtension('platformOverride', {'value': 'iOS'})); expect(result, {'value': 'iOS'}); expect(binding.reassembled, 5); expect(defaultTargetPlatform, TargetPlatform.iOS); expect(extensionChangedEvents.length, 5); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'iOS'); result = await hasReassemble(binding.testExtension('platformOverride', {'value': 'bogus'})); expect(result, {'value': 'android'}); expect(binding.reassembled, 6); expect(defaultTargetPlatform, TargetPlatform.android); expect(extensionChangedEvents.length, 6); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.platformOverride'); expect(extensionChangedEvent['value'], 'android'); binding.reassembled = 0; }); test('Service extensions - repaintRainbow', () async { Map result; Future> pendingResult; bool completed; expect(binding.frameScheduled, isFalse); expect(debugRepaintRainbowEnabled, false); result = await binding.testExtension('repaintRainbow', {}); expect(result, { 'enabled': 'false' }); expect(debugRepaintRainbowEnabled, false); expect(binding.frameScheduled, isFalse); pendingResult = binding.testExtension('repaintRainbow', { 'enabled': 'true' }); completed = false; pendingResult.whenComplete(() { completed = true; }); await binding.flushMicrotasks(); expect(completed, true); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, { 'enabled': 'true' }); expect(debugRepaintRainbowEnabled, true); result = await binding.testExtension('repaintRainbow', {}); expect(result, { 'enabled': 'true' }); expect(debugRepaintRainbowEnabled, true); expect(binding.frameScheduled, isFalse); pendingResult = binding.testExtension('repaintRainbow', { 'enabled': 'false' }); completed = false; pendingResult.whenComplete(() { completed = true; }); await binding.flushMicrotasks(); expect(completed, false); expect(binding.frameScheduled, isTrue); await binding.doFrame(); await binding.flushMicrotasks(); expect(completed, true); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, { 'enabled': 'false' }); expect(debugRepaintRainbowEnabled, false); result = await binding.testExtension('repaintRainbow', {}); expect(result, { 'enabled': 'false' }); expect(debugRepaintRainbowEnabled, false); expect(binding.frameScheduled, isFalse); }); test('Service extensions - reassemble', () async { Map result; Future> pendingResult; bool completed; completed = false; expect(binding.reassembled, 0); pendingResult = binding.testExtension('reassemble', {}); pendingResult.whenComplete(() { completed = true; }); await binding.flushMicrotasks(); expect(binding.frameScheduled, isTrue); expect(completed, false); await binding.flushMicrotasks(); await binding.doFrame(); await binding.flushMicrotasks(); expect(completed, true); expect(binding.frameScheduled, isFalse); result = await pendingResult; expect(result, {}); expect(binding.reassembled, 1); }); test('Service extensions - showPerformanceOverlay', () async { Map result; expect(binding.frameScheduled, isFalse); expect(WidgetsApp.showPerformanceOverlayOverride, false); result = await binding.testExtension('showPerformanceOverlay', {}); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.showPerformanceOverlayOverride, false); result = await binding.testExtension('showPerformanceOverlay', { 'enabled': 'true' }); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.showPerformanceOverlayOverride, true); result = await binding.testExtension('showPerformanceOverlay', {}); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.showPerformanceOverlayOverride, true); result = await binding.testExtension('showPerformanceOverlay', { 'enabled': 'false' }); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.showPerformanceOverlayOverride, false); result = await binding.testExtension('showPerformanceOverlay', {}); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.showPerformanceOverlayOverride, false); expect(binding.frameScheduled, isFalse); }); test('Service extensions - debugWidgetInspector', () async { Map result; expect(binding.frameScheduled, isFalse); expect(WidgetsApp.debugShowWidgetInspectorOverride, false); result = await binding.testExtension('debugWidgetInspector', {}); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.debugShowWidgetInspectorOverride, false); result = await binding.testExtension('debugWidgetInspector', { 'enabled': 'true' }); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.debugShowWidgetInspectorOverride, true); result = await binding.testExtension('debugWidgetInspector', {}); expect(result, { 'enabled': 'true' }); expect(WidgetsApp.debugShowWidgetInspectorOverride, true); result = await binding.testExtension('debugWidgetInspector', { 'enabled': 'false' }); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.debugShowWidgetInspectorOverride, false); result = await binding.testExtension('debugWidgetInspector', {}); expect(result, { 'enabled': 'false' }); expect(WidgetsApp.debugShowWidgetInspectorOverride, false); expect(binding.frameScheduled, isFalse); }); test('Service extensions - timeDilation', () async { final Iterable> extensionChangedEvents = binding.getServiceExtensionStateChangedEvents('ext.flutter.timeDilation'); Map extensionChangedEvent; Map result; expect(binding.frameScheduled, isFalse); expect(timeDilation, 1.0); result = await binding.testExtension('timeDilation', {}); expect(result, { 'timeDilation': '1.0' }); expect(timeDilation, 1.0); expect(extensionChangedEvents, isEmpty); result = await binding.testExtension('timeDilation', { 'timeDilation': '100.0' }); expect(result, { 'timeDilation': '100.0' }); expect(timeDilation, 100.0); expect(extensionChangedEvents.length, 1); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.timeDilation'); expect(extensionChangedEvent['value'], '100.0'); result = await binding.testExtension('timeDilation', {}); expect(result, { 'timeDilation': '100.0' }); expect(timeDilation, 100.0); expect(extensionChangedEvents.length, 1); result = await binding.testExtension('timeDilation', { 'timeDilation': '1.0' }); expect(result, { 'timeDilation': '1.0' }); expect(timeDilation, 1.0); expect(extensionChangedEvents.length, 2); extensionChangedEvent = extensionChangedEvents.last; expect(extensionChangedEvent['extension'], 'ext.flutter.timeDilation'); expect(extensionChangedEvent['value'], '1.0'); result = await binding.testExtension('timeDilation', {}); expect(result, { 'timeDilation': '1.0' }); expect(timeDilation, 1.0); expect(extensionChangedEvents.length, 2); expect(binding.frameScheduled, isFalse); }); test('Service extensions - saveCompilationTrace', () async { Map result; result = await binding.testExtension('saveCompilationTrace', {}); final String trace = String.fromCharCodes(result['value']); expect(trace, contains('dart:core,Object,Object.\n')); expect(trace, contains('package:test_api/test_api.dart,::,test\n')); expect(trace, contains('service_extensions_test.dart,::,main\n')); }); test('Service extensions - posttest', () async { // See widget_inspector_test.dart for tests of the ext.flutter.inspector // service extensions included in this count. int widgetInspectorExtensionCount = 15; if (WidgetInspectorService.instance.isWidgetCreationTracked()) { // Some inspector extensions are only exposed if widget creation locations // are tracked. widgetInspectorExtensionCount += 2; } // If you add a service extension... TEST IT! :-) // ...then increment this number. expect(binding.extensions.length, 25 + widgetInspectorExtensionCount); expect(console, isEmpty); debugPrint = debugPrintThrottled; }); }