@@ -11,7 +11,7 @@ import 'widget_test_stubs.dart';
1111/// implementing all [StreamChannelMixin] methods.
1212class FakeWebSocketChannel extends Fake implements WebSocketChannel {
1313 final Completer <void > readyCompleter;
14- final FakeWebSocketSink fakeSink = FakeWebSocketSink ();
14+ late final FakeWebSocketSink fakeSink = FakeWebSocketSink (_streamController );
1515 final StreamController <dynamic > _streamController =
1616 StreamController <dynamic >.broadcast ();
1717
@@ -35,17 +35,23 @@ class FakeWebSocketChannel extends Fake implements WebSocketChannel {
3535}
3636
3737class FakeWebSocketSink extends Fake implements WebSocketSink {
38+ final StreamController <dynamic > _streamController;
3839 final Completer <void > _doneCompleter = Completer <void >();
3940 int ? closeCode;
4041 String ? closeReason;
4142
43+ FakeWebSocketSink (this ._streamController);
44+
4245 @override
4346 Future <void > close ([int ? code, String ? reason]) async {
4447 closeCode = code;
4548 closeReason = reason;
4649 if (! _doneCompleter.isCompleted) {
4750 _doneCompleter.complete ();
4851 }
52+ if (! _streamController.isClosed) {
53+ await _streamController.close ();
54+ }
4955 }
5056
5157 @override
@@ -68,22 +74,10 @@ void main() {
6874 const supabaseKey = '' ;
6975
7076 group ('Lifecycle realtime reconnection' , () {
71- late List <FakeWebSocketChannel > createdChannels;
7277 late List <Completer <void >> readyCompleters;
7378
74- setUp (() {
75- mockAppLink ();
76- createdChannels = [];
79+ setUp (() async {
7780 readyCompleters = [];
78- });
79-
80- tearDown (() async {
81- try {
82- await Supabase .instance.dispose ();
83- } catch (_) {}
84- });
85-
86- Future <void > initWithMockTransport () async {
8781 await Supabase .initialize (
8882 url: supabaseUrl,
8983 anonKey: supabaseKey,
@@ -96,13 +90,17 @@ void main() {
9690 transport: (url, headers) {
9791 final completer = Completer <void >();
9892 readyCompleters.add (completer);
99- final channel = FakeWebSocketChannel (readyCompleter: completer);
100- createdChannels.add (channel);
101- return channel;
93+ return FakeWebSocketChannel (readyCompleter: completer);
10294 },
10395 ),
10496 );
105- }
97+ });
98+
99+ tearDown (() async {
100+ try {
101+ await Supabase .instance.dispose ();
102+ } catch (_) {}
103+ });
106104
107105 /// Helper: call connect() and immediately complete the
108106 /// ready future created by the transport factory.
@@ -114,11 +112,19 @@ void main() {
114112 await future;
115113 }
116114
115+ // Helper: complete all pending ready futures to unblock connect()
116+ void completeReadyCompleters () async {
117+ for (final completer in readyCompleters) {
118+ if (! completer.isCompleted) completer.complete ();
119+ }
120+ readyCompleters.clear ();
121+ }
122+
117123 test (
118124 'paused then resumed waits for disconnect '
119125 'before reconnecting' , () async {
120- await initWithMockTransport ();
121126 final realtime = Supabase .instance.client.realtime;
127+ final binding = TestWidgetsFlutterBinding .instance;
122128
123129 // Add a channel so onResumed() processes reconnection
124130 realtime.channel ('test' );
@@ -128,123 +134,124 @@ void main() {
128134 expect (realtime.connState, SocketStates .open);
129135
130136 // paused → triggers disconnect
131- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .paused);
132- await Future <void >.delayed (Duration .zero);
137+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
138+ binding.handleAppLifecycleStateChanged (AppLifecycleState .hidden);
139+ binding.handleAppLifecycleStateChanged (AppLifecycleState .paused);
133140
134141 // resumed → waits for disconnect, then reconnects
135- Supabase .instance. didChangeAppLifecycleState (AppLifecycleState .resumed );
136- await Future < void >. delayed ( Duration .zero );
142+ binding. handleAppLifecycleStateChanged (AppLifecycleState .detached );
143+ binding. handleAppLifecycleStateChanged ( AppLifecycleState .resumed );
137144
138145 // Complete any pending ready futures (reconnect)
139- for (final c in readyCompleters) {
140- if (! c.isCompleted) c.complete ();
141- }
142- await Future <void >.delayed (const Duration (milliseconds: 100 ));
146+ completeReadyCompleters ();
147+ await pumpEventQueue ();
143148
144149 expect (realtime.connState, SocketStates .open);
150+ expect (realtime.conn, isNotNull);
145151 });
146152
147153 test (
148154 'paused → resumed → inactive → resumed '
149155 'still reconnects' , () async {
150- await initWithMockTransport ();
151156 final realtime = Supabase .instance.client.realtime;
157+ final binding = TestWidgetsFlutterBinding .instance;
152158
153159 realtime.channel ('test' );
154160
155161 await connectAndReady (realtime);
156162 expect (realtime.connState, SocketStates .open);
157163
158164 // paused → starts disconnect
159- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .paused);
160- await Future <void >.delayed (Duration .zero);
165+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
166+ binding.handleAppLifecycleStateChanged (AppLifecycleState .hidden);
167+ binding.handleAppLifecycleStateChanged (AppLifecycleState .paused);
161168
162169 // first resumed → queues reconnect after disconnect
163- Supabase .instance. didChangeAppLifecycleState (AppLifecycleState .resumed );
164- await Future < void >. delayed ( Duration .zero );
170+ binding. handleAppLifecycleStateChanged (AppLifecycleState .detached );
171+ binding. handleAppLifecycleStateChanged ( AppLifecycleState .resumed );
165172
166173 // inactive → does nothing (not a tracked lifecycle state)
167- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .inactive);
168- await Future <void >.delayed (Duration .zero);
174+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
169175
170176 // second resumed → queues another reconnect (idempotent)
171- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .resumed);
172- await Future <void >.delayed (Duration .zero);
177+ binding.handleAppLifecycleStateChanged (AppLifecycleState .resumed);
173178
174179 // Complete all pending ready futures
175- for (final c in readyCompleters) {
176- if (! c.isCompleted) c.complete ();
177- }
178- await Future <void >.delayed (const Duration (milliseconds: 100 ));
180+ completeReadyCompleters ();
181+ await pumpEventQueue ();
179182
180183 // Should have reconnected, not stuck disconnecting
181- expect (realtime.connState, isNot ( SocketStates .disconnecting) );
184+ expect (realtime.connState, SocketStates .open );
182185 expect (realtime.conn, isNotNull);
183186 });
184187
185188 test (
186189 'rapid paused → resumed → paused → resumed '
187190 'ends up connected' , () async {
188- await initWithMockTransport ();
189191 final realtime = Supabase .instance.client.realtime;
192+ final binding = TestWidgetsFlutterBinding .instance;
190193
191194 realtime.channel ('test' );
192195
193196 await connectAndReady (realtime);
194197 expect (realtime.connState, SocketStates .open);
195198
196199 // Rapid lifecycle flapping
197- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .paused);
198- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .resumed);
199- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .paused);
200- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .resumed);
200+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
201+ binding.handleAppLifecycleStateChanged (AppLifecycleState .hidden);
202+ binding.handleAppLifecycleStateChanged (AppLifecycleState .paused);
201203
202- // Complete all pending ready futures as they appear
203- for (final c in readyCompleters) {
204- if (! c.isCompleted) c.complete ();
205- }
206- await Future <void >.delayed (const Duration (milliseconds: 200 ));
204+ binding.handleAppLifecycleStateChanged (AppLifecycleState .detached);
205+ binding.handleAppLifecycleStateChanged (AppLifecycleState .resumed);
207206
208- // Also complete any new ready futures created during processing
209- for (final c in readyCompleters) {
210- if (! c.isCompleted) c.complete ();
211- }
212- await Future <void >.delayed (const Duration (milliseconds: 200 ));
207+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
208+ binding.handleAppLifecycleStateChanged (AppLifecycleState .hidden);
209+ binding.handleAppLifecycleStateChanged (AppLifecycleState .paused);
210+
211+ binding.handleAppLifecycleStateChanged (AppLifecycleState .detached);
212+ binding.handleAppLifecycleStateChanged (AppLifecycleState .resumed);
213+
214+ // Complete all pending ready futures as they appear
215+ completeReadyCompleters ();
216+ await pumpEventQueue ();
213217
214218 expect (realtime.connState, SocketStates .open);
219+ expect (realtime.conn, isNotNull);
215220 });
216221
217222 test (
218223 'resumed then paused before connect completes '
219224 'cancels reconnect' , () async {
220- await initWithMockTransport ();
221225 final realtime = Supabase .instance.client.realtime;
226+ final binding = TestWidgetsFlutterBinding .instance;
222227
223228 realtime.channel ('test' );
224229
225230 await connectAndReady (realtime);
226231 expect (realtime.connState, SocketStates .open);
227232
228233 // paused → triggers disconnect
229- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .paused);
230- await Future <void >.delayed (Duration .zero);
234+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
235+ binding.handleAppLifecycleStateChanged (AppLifecycleState .hidden);
236+ binding.handleAppLifecycleStateChanged (AppLifecycleState .paused);
231237
232238 // resumed → queues reconnect
233- Supabase .instance. didChangeAppLifecycleState (AppLifecycleState .resumed );
234- await Future < void >. delayed ( Duration .zero );
239+ binding. handleAppLifecycleStateChanged (AppLifecycleState .detached );
240+ binding. handleAppLifecycleStateChanged ( AppLifecycleState .resumed );
235241
236242 // paused again before connect completes → should cancel the
237243 // reconnect (target state is now paused)
238- Supabase .instance.didChangeAppLifecycleState (AppLifecycleState .paused);
244+ binding.handleAppLifecycleStateChanged (AppLifecycleState .inactive);
245+ binding.handleAppLifecycleStateChanged (AppLifecycleState .hidden);
246+ binding.handleAppLifecycleStateChanged (AppLifecycleState .paused);
239247
240248 // Complete all pending ready futures
241- for (final c in readyCompleters) {
242- if (! c.isCompleted) c.complete ();
243- }
244- await Future <void >.delayed (const Duration (milliseconds: 200 ));
249+ completeReadyCompleters ();
250+ await pumpEventQueue ();
245251
246252 // Should be disconnected since the last event was paused
247253 expect (realtime.connState, SocketStates .disconnected);
254+ expect (realtime.conn, isNull);
248255 });
249256 });
250257}
0 commit comments