1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:ui'; |
6 | |
7 | import 'package:flutter/services.dart'; |
8 | import 'package:flutter/widgets.dart'; |
9 | import 'package:flutter_test/flutter_test.dart'; |
10 | import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart' ; |
11 | |
12 | void main() { |
13 | AppLifecycleListener? listener; |
14 | |
15 | Future<void> setAppLifeCycleState(AppLifecycleState state) async { |
16 | final ByteData? message = const StringCodec().encodeMessage(state.toString()); |
17 | await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( |
18 | 'flutter/lifecycle' , |
19 | message, |
20 | (_) {}, |
21 | ); |
22 | } |
23 | |
24 | Future<void> sendAppExitRequest() async { |
25 | final ByteData message = const JSONMethodCodec().encodeMethodCall( |
26 | const MethodCall('System.requestAppExit' ), |
27 | ); |
28 | await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( |
29 | 'flutter/platform' , |
30 | message, |
31 | (_) {}, |
32 | ); |
33 | } |
34 | |
35 | setUp(() async { |
36 | WidgetsFlutterBinding.ensureInitialized(); |
37 | WidgetsBinding.instance |
38 | ..resetEpoch() |
39 | ..platformDispatcher.onBeginFrame = null |
40 | ..platformDispatcher.onDrawFrame = null; |
41 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; |
42 | binding.readTestInitialLifecycleStateFromNativeWindow(); |
43 | // Reset the state to detached. Going to paused first makes it a valid |
44 | // transition from any state, since the intermediate transitions will be |
45 | // generated. |
46 | await setAppLifeCycleState(AppLifecycleState.paused); |
47 | await setAppLifeCycleState(AppLifecycleState.detached); |
48 | }); |
49 | |
50 | tearDown(() { |
51 | listener?.dispose(); |
52 | listener = null; |
53 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; |
54 | binding.resetInternalState(); |
55 | binding.platformDispatcher.resetInitialLifecycleState(); |
56 | assert( |
57 | TestAppLifecycleListener.registerCount == 0, |
58 | 'There were ${TestAppLifecycleListener.registerCount} listeners that were not disposed of in tests.' , |
59 | ); |
60 | }); |
61 | |
62 | testWidgets('Default Diagnostics' , (WidgetTester tester) async { |
63 | listener = TestAppLifecycleListener(binding: tester.binding); |
64 | expect( |
65 | listener.toString(), |
66 | equalsIgnoringHashCodes( |
67 | 'TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>)' , |
68 | ), |
69 | ); |
70 | }); |
71 | |
72 | testWidgets('Diagnostics' , (WidgetTester tester) async { |
73 | Future<AppExitResponse> handleExitRequested() async { |
74 | return AppExitResponse.cancel; |
75 | } |
76 | |
77 | listener = TestAppLifecycleListener( |
78 | binding: WidgetsBinding.instance, |
79 | onExitRequested: handleExitRequested, |
80 | onStateChange: (AppLifecycleState _) {}, |
81 | ); |
82 | expect( |
83 | listener.toString(), |
84 | equalsIgnoringHashCodes( |
85 | 'TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>, onStateChange, onExitRequested)' , |
86 | ), |
87 | ); |
88 | }); |
89 | |
90 | testWidgets('listens to AppLifecycleState' , (WidgetTester tester) async { |
91 | final List<AppLifecycleState> states = <AppLifecycleState>[tester.binding.lifecycleState!]; |
92 | void stateChange(AppLifecycleState state) { |
93 | states.add(state); |
94 | } |
95 | |
96 | listener = TestAppLifecycleListener( |
97 | binding: WidgetsBinding.instance, |
98 | onStateChange: stateChange, |
99 | ); |
100 | expect(states, equals(<AppLifecycleState>[AppLifecycleState.detached])); |
101 | await setAppLifeCycleState(AppLifecycleState.inactive); |
102 | // "resumed" is generated. |
103 | expect( |
104 | states, |
105 | equals(<AppLifecycleState>[ |
106 | AppLifecycleState.detached, |
107 | AppLifecycleState.resumed, |
108 | AppLifecycleState.inactive, |
109 | ]), |
110 | ); |
111 | await setAppLifeCycleState(AppLifecycleState.resumed); |
112 | expect( |
113 | states, |
114 | equals(<AppLifecycleState>[ |
115 | AppLifecycleState.detached, |
116 | AppLifecycleState.resumed, |
117 | AppLifecycleState.inactive, |
118 | AppLifecycleState.resumed, |
119 | ]), |
120 | ); |
121 | }); |
122 | |
123 | testWidgets('Triggers correct state transition callbacks' , (WidgetTester tester) async { |
124 | final List<String> transitions = <String>[]; |
125 | listener = TestAppLifecycleListener( |
126 | binding: WidgetsBinding.instance, |
127 | onDetach: () => transitions.add('detach' ), |
128 | onHide: () => transitions.add('hide' ), |
129 | onInactive: () => transitions.add('inactive' ), |
130 | onPause: () => transitions.add('pause' ), |
131 | onRestart: () => transitions.add('restart' ), |
132 | onResume: () => transitions.add('resume' ), |
133 | onShow: () => transitions.add('show' ), |
134 | ); |
135 | |
136 | // Try "standard" sequence |
137 | await setAppLifeCycleState(AppLifecycleState.resumed); |
138 | expect(transitions, equals(<String>['resume' ])); |
139 | await setAppLifeCycleState(AppLifecycleState.inactive); |
140 | expect(transitions, equals(<String>['resume' , 'inactive' ])); |
141 | await setAppLifeCycleState(AppLifecycleState.hidden); |
142 | expect(transitions, equals(<String>['resume' , 'inactive' , 'hide' ])); |
143 | await setAppLifeCycleState(AppLifecycleState.paused); |
144 | expect(transitions, equals(<String>['resume' , 'inactive' , 'hide' , 'pause' ])); |
145 | |
146 | // Go back to resume |
147 | transitions.clear(); |
148 | await setAppLifeCycleState(AppLifecycleState.hidden); |
149 | expect(transitions, equals(<String>['restart' ])); |
150 | await setAppLifeCycleState(AppLifecycleState.inactive); |
151 | expect(transitions, equals(<String>['restart' , 'show' ])); |
152 | await setAppLifeCycleState(AppLifecycleState.resumed); |
153 | expect(transitions, equals(<String>['restart' , 'show' , 'resume' ])); |
154 | |
155 | // Generates intermediate states from lower to higher lifecycle states. |
156 | transitions.clear(); |
157 | await setAppLifeCycleState(AppLifecycleState.paused); |
158 | expect(transitions, equals(<String>['inactive' , 'hide' , 'pause' ])); |
159 | |
160 | // Wraps around from pause to detach. |
161 | await setAppLifeCycleState(AppLifecycleState.detached); |
162 | expect(transitions, equals(<String>['inactive' , 'hide' , 'pause' , 'detach' ])); |
163 | await setAppLifeCycleState(AppLifecycleState.resumed); |
164 | expect(transitions, equals(<String>['inactive' , 'hide' , 'pause' , 'detach' , 'resume' ])); |
165 | await setAppLifeCycleState(AppLifecycleState.paused); |
166 | expect( |
167 | transitions, |
168 | equals(<String>[ |
169 | 'inactive' , |
170 | 'hide' , |
171 | 'pause' , |
172 | 'detach' , |
173 | 'resume' , |
174 | 'inactive' , |
175 | 'hide' , |
176 | 'pause' , |
177 | ]), |
178 | ); |
179 | |
180 | // Generates intermediate states from higher to lower lifecycle states. |
181 | transitions.clear(); |
182 | await setAppLifeCycleState(AppLifecycleState.resumed); |
183 | expect(transitions, equals(<String>['restart' , 'show' , 'resume' ])); |
184 | |
185 | // Go to detached |
186 | transitions.clear(); |
187 | await setAppLifeCycleState(AppLifecycleState.detached); |
188 | expect(transitions, equals(<String>['inactive' , 'hide' , 'pause' , 'detach' ])); |
189 | }); |
190 | |
191 | testWidgets('Receives exit requests' , (WidgetTester tester) async { |
192 | bool exitRequested = false; |
193 | Future<AppExitResponse> handleExitRequested() async { |
194 | exitRequested = true; |
195 | return AppExitResponse.cancel; |
196 | } |
197 | |
198 | listener = TestAppLifecycleListener( |
199 | binding: WidgetsBinding.instance, |
200 | onExitRequested: handleExitRequested, |
201 | ); |
202 | await sendAppExitRequest(); |
203 | expect(exitRequested, isTrue); |
204 | }); |
205 | |
206 | test('AppLifecycleListener dispatches memory events' , () async { |
207 | await expectLater( |
208 | await memoryEvents( |
209 | () => AppLifecycleListener(binding: WidgetsBinding.instance).dispose(), |
210 | AppLifecycleListener, |
211 | ), |
212 | areCreateAndDispose, |
213 | ); |
214 | }); |
215 | } |
216 | |
217 | class TestAppLifecycleListener extends AppLifecycleListener { |
218 | TestAppLifecycleListener({ |
219 | super.binding, |
220 | super.onResume, |
221 | super.onInactive, |
222 | super.onHide, |
223 | super.onShow, |
224 | super.onPause, |
225 | super.onRestart, |
226 | super.onDetach, |
227 | super.onExitRequested, |
228 | super.onStateChange, |
229 | }) { |
230 | registerCount += 1; |
231 | } |
232 | |
233 | static int registerCount = 0; |
234 | |
235 | @override |
236 | void dispose() { |
237 | super.dispose(); |
238 | registerCount -= 1; |
239 | } |
240 | } |
241 | |