// Copyright 2014 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:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; class PrintOverrideTestBinding extends AutomatedTestWidgetsFlutterBinding { @override DebugPrintCallback get debugPrintOverride => _enablePrint ? debugPrint : _emptyPrint; static void _emptyPrint(String? message, { int? wrapWidth }) {} static bool _enablePrint = true; static void runWithDebugPrintDisabled(void Function() f) { try { _enablePrint = false; f(); } finally { _enablePrint = true; } } } void main() { final MemoryAllocations ma = MemoryAllocations.instance; PrintOverrideTestBinding(); setUp(() { assert(!ma.hasListeners); _checkSdkHandlersNotSet(); }); test('addListener and removeListener add and remove listeners.', () { final ObjectEvent event = ObjectDisposed(object: 'object'); ObjectEvent? recievedEvent; void listener(ObjectEvent event) => recievedEvent = event; expect(ma.hasListeners, isFalse); ma.addListener(listener); _checkSdkHandlersSet(); ma.dispatchObjectEvent(event); expect(recievedEvent, equals(event)); expect(ma.hasListeners, isTrue); recievedEvent = null; ma.removeListener(listener); ma.dispatchObjectEvent(event); expect(recievedEvent, isNull); expect(ma.hasListeners, isFalse); _checkSdkHandlersNotSet(); }); testWidgets('dispatchObjectEvent handles bad listeners', (WidgetTester tester) async { final ObjectEvent event = ObjectDisposed(object: 'object'); final List<String> log = <String>[]; void badListener1(ObjectEvent event) { log.add('badListener1'); throw ArgumentError(); } void listener1(ObjectEvent event) => log.add('listener1'); void badListener2(ObjectEvent event) { log.add('badListener2'); throw ArgumentError(); } void listener2(ObjectEvent event) => log.add('listener2'); ma.addListener(badListener1); _checkSdkHandlersSet(); ma.addListener(listener1); ma.addListener(badListener2); ma.addListener(listener2); PrintOverrideTestBinding.runWithDebugPrintDisabled( () => ma.dispatchObjectEvent(event) ); expect(log, <String>['badListener1', 'listener1', 'badListener2','listener2']); expect(tester.takeException(), contains('Multiple exceptions (2)')); ma.removeListener(badListener1); _checkSdkHandlersSet(); ma.removeListener(listener1); ma.removeListener(badListener2); ma.removeListener(listener2); _checkSdkHandlersNotSet(); log.clear(); expect(ma.hasListeners, isFalse); ma.dispatchObjectEvent(event); expect(log, <String>[]); }); test('dispatchObjectEvent does not invoke concurrently added listeners', () { final ObjectEvent event = ObjectDisposed(object: 'object'); final List<String> log = <String>[]; void listener2(ObjectEvent event) => log.add('listener2'); void listener1(ObjectEvent event) { log.add('listener1'); ma.addListener(listener2); } ma.addListener(listener1); _checkSdkHandlersSet(); ma.dispatchObjectEvent(event); expect(log, <String>['listener1']); log.clear(); ma.dispatchObjectEvent(event); expect(log, <String>['listener1','listener2']); log.clear(); ma.removeListener(listener1); ma.removeListener(listener2); _checkSdkHandlersNotSet(); expect(ma.hasListeners, isFalse); ma.dispatchObjectEvent(event); expect(log, <String>[]); }); test('dispatchObjectEvent does not invoke concurrently removed listeners', () { final ObjectEvent event = ObjectDisposed(object: 'object'); final List<String> log = <String>[]; void listener2(ObjectEvent event) => log.add('listener2'); void listener1(ObjectEvent event) { log.add('listener1'); ma.removeListener(listener2); expect(ma.hasListeners, isFalse); } ma.addListener(listener1); ma.addListener(listener2); ma.dispatchObjectEvent(event); expect(log, <String>['listener1']); log.clear(); ma.removeListener(listener1); _checkSdkHandlersNotSet(); expect(ma.hasListeners, isFalse); }); test('last removeListener unsubscribes from Flutter SDK events', () { void listener1(ObjectEvent event) {} void listener2(ObjectEvent event) {} ma.addListener(listener1); _checkSdkHandlersSet(); ma.addListener(listener2); _checkSdkHandlersSet(); ma.removeListener(listener1); _checkSdkHandlersSet(); ma.removeListener(listener2); _checkSdkHandlersNotSet(); }); test('kFlutterMemoryAllocationsEnabled is true in debug mode.', () { expect(kFlutterMemoryAllocationsEnabled, isTrue); }); test('publishers in Flutter dispatch events in debug mode', () async { int eventCount = 0; void listener(ObjectEvent event) => eventCount++; ma.addListener(listener); final int expectedEventCount = await _activateFlutterObjectsAndReturnCountOfEvents(); expect(eventCount, expectedEventCount); ma.removeListener(listener); _checkSdkHandlersNotSet(); expect(ma.hasListeners, isFalse); }); } void _checkSdkHandlersSet() { expect(Image.onCreate, isNotNull); expect(Picture.onCreate, isNotNull); expect(Image.onDispose, isNotNull); expect(Picture.onDispose, isNotNull); } void _checkSdkHandlersNotSet() { expect(Image.onCreate, isNull); expect(Picture.onCreate, isNull); expect(Image.onDispose, isNull); expect(Picture.onDispose, isNull); } /// Create and dispose Flutter objects to fire memory allocation events. Future<int> _activateFlutterObjectsAndReturnCountOfEvents() async { int count = 0; final ValueNotifier<bool> valueNotifier = ValueNotifier<bool>(true); count++; final ChangeNotifier changeNotifier = ChangeNotifier()..addListener(() {}); count++; final Picture picture = _createPicture(); count++; valueNotifier.dispose(); count++; changeNotifier.dispose(); count++; picture.dispose(); count++; // TODO(polina-c): Remove the condition after // https://github.com/flutter/flutter/issues/110599 is fixed. if (!kIsWeb) { final Image image = await _createImage(); count++; count++; count++; image.dispose(); count++; } return count; } Future<Image> _createImage() async { final Picture picture = _createPicture(); final Image result = await picture.toImage(10, 10); picture.dispose(); return result; } Picture _createPicture() { final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); canvas.clipRect(rect); return recorder.endRecording(); }