@@ -18,13 +18,19 @@ SocketClient getTestClient(
1818 bool autoReconnect = true ,
1919 Map <String , dynamic >? customHeaders,
2020 Duration delayBetweenReconnectionAttempts =
21- const Duration (milliseconds: 1 )}) =>
21+ const Duration (milliseconds: 1 ),
22+ String protocol = SocketSubProtocol .graphqlWs,
23+ }) =>
2224 SocketClient (
2325 wsUrl,
26+ protocol: protocol,
2427 config: SocketClientConfig (
2528 autoReconnect: autoReconnect,
2629 headers: customHeaders,
2730 delayBetweenReconnectionAttempts: delayBetweenReconnectionAttempts,
31+ initialPayload: {
32+ 'protocol' : protocol,
33+ },
2834 ),
2935 randomBytesForUuid: Uint8List .fromList (
3036 [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 ],
@@ -324,6 +330,274 @@ Future<void> main() async {
324330 });
325331 }, tags: "integration" );
326332
333+ group ('SocketClient without payload graphql-transport-ws' , () {
334+ late SocketClient socketClient;
335+ StreamController <dynamic > controller;
336+ final expectedMessage = r'{'
337+ r'"type":"subscribe","id":"01020304-0506-4708-890a-0b0c0d0e0f10",'
338+ r'"payload":{"operationName":null,"variables":{},"query":"subscription {\n \n}"}'
339+ r'}' ;
340+ setUp (overridePrint ((log) {
341+ controller = StreamController (sync : true );
342+ socketClient = getTestClient (
343+ controller: controller,
344+ protocol: SocketSubProtocol .graphqlTransportWs,
345+ wsUrl: wsUrl,
346+ );
347+ }));
348+ tearDown (overridePrint (
349+ (log) => socketClient.dispose (),
350+ ));
351+ test ('connection graphql-transport-ws' , () async {
352+ await expectLater (
353+ socketClient.connectionState.asBroadcastStream (),
354+ emitsInOrder (
355+ [
356+ SocketConnectionState .connecting,
357+ SocketConnectionState .handshake,
358+ SocketConnectionState .connected,
359+ ],
360+ ),
361+ );
362+ });
363+ test ('disconnect via dispose graphql-transport-ws' , () async {
364+ // First wait for connection to complete
365+ await expectLater (
366+ socketClient.connectionState.asBroadcastStream (),
367+ emitsInOrder (
368+ [
369+ SocketConnectionState .connecting,
370+ SocketConnectionState .handshake,
371+ SocketConnectionState .connected,
372+ ],
373+ ),
374+ );
375+
376+ // We need to begin waiting on the connectionState
377+ // before we issue the command to disconnect; otherwise
378+ // it can reconnect so fast that it will be reconnected
379+ // by the time that the expectLater check is initiated.
380+ await overridePrint ((_) async {
381+ Timer (const Duration (milliseconds: 20 ), () async {
382+ await socketClient.dispose ();
383+ });
384+ })();
385+ // The connectionState BehaviorController emits the current state
386+ // to any new listener, so we expect it to start in the connected
387+ // state and transition to notConnected because of dispose.
388+ await expectLater (
389+ socketClient.connectionState,
390+ emitsInOrder ([
391+ SocketConnectionState .connected,
392+ SocketConnectionState .notConnected,
393+ ]),
394+ );
395+
396+ // Have to wait for socket close to be fully processed after we reach
397+ // the notConnected state, including updating channel with close code.
398+ await Future <void >.delayed (const Duration (milliseconds: 20 ));
399+
400+ // The websocket should be in a fully closed state at this point,
401+ // we should have a confirmed close code in the channel.
402+ expect (socketClient.socketChannel, isNotNull);
403+ expect (socketClient.socketChannel! .closeCode, isNotNull);
404+ });
405+ test ('subscription data graphql-transport-ws' , () async {
406+ final payload = Request (
407+ operation: Operation (document: parseString ('subscription {}' )),
408+ );
409+ final waitForConnection = true ;
410+ final subscriptionDataStream =
411+ socketClient.subscribe (payload, waitForConnection);
412+ await socketClient.connectionState
413+ .where ((state) => state == SocketConnectionState .connected)
414+ .first;
415+
416+ // ignore: unawaited_futures
417+ socketClient.socketChannel! .stream
418+ .where ((message) => message == expectedMessage)
419+ .first
420+ .then ((_) {
421+ socketClient.socketChannel! .sink.add (jsonEncode ({
422+ 'type' : 'next' ,
423+ 'id' : '01020304-0506-4708-890a-0b0c0d0e0f10' ,
424+ 'payload' : {
425+ 'data' : {'foo' : 'bar' },
426+ 'errors' : [
427+ {'message' : 'error and data can coexist' }
428+ ]
429+ }
430+ }));
431+ });
432+
433+ await expectLater (
434+ subscriptionDataStream,
435+ emits (
436+ // todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
437+ Response (
438+ data: {'foo' : 'bar' },
439+ errors: [
440+ GraphQLError (message: 'error and data can coexist' ),
441+ ],
442+ context: Context ().withEntry (ResponseExtensions (null )),
443+ response: {
444+ "type" : "next" ,
445+ "data" : {"foo" : "bar" },
446+ "errors" : [
447+ {"message" : "error and data can coexist" }
448+ ]
449+ },
450+ ),
451+ ),
452+ );
453+ });
454+ test ('resubscribe' , () async {
455+ final payload = Request (
456+ operation: Operation (document: gql ('subscription {}' )),
457+ );
458+ final waitForConnection = true ;
459+ final subscriptionDataStream =
460+ socketClient.subscribe (payload, waitForConnection);
461+
462+ await expectLater (
463+ socketClient.connectionState,
464+ emitsInOrder ([
465+ SocketConnectionState .connecting,
466+ SocketConnectionState .handshake,
467+ SocketConnectionState .connected,
468+ ]),
469+ );
470+
471+ await overridePrint ((_) async {
472+ socketClient.onConnectionLost ();
473+ })();
474+
475+ await expectLater (
476+ socketClient.connectionState,
477+ emitsInOrder ([
478+ SocketConnectionState .notConnected,
479+ SocketConnectionState .connecting,
480+ SocketConnectionState .handshake,
481+ SocketConnectionState .connected,
482+ ]),
483+ );
484+
485+ // ignore: unawaited_futures
486+ socketClient.socketChannel! .stream
487+ .where ((message) => message == expectedMessage)
488+ .first
489+ .then ((_) {
490+ socketClient.socketChannel! .sink.add (jsonEncode ({
491+ 'type' : 'next' ,
492+ 'id' : '01020304-0506-4708-890a-0b0c0d0e0f10' ,
493+ 'payload' : {
494+ 'data' : {'foo' : 'bar' },
495+ 'errors' : [
496+ {'message' : 'error and data can coexist' }
497+ ]
498+ }
499+ }));
500+ });
501+
502+ await expectLater (
503+ subscriptionDataStream,
504+ emits (
505+ // todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
506+ Response (
507+ data: {'foo' : 'bar' },
508+ errors: [
509+ GraphQLError (message: 'error and data can coexist' ),
510+ ],
511+ context: Context ().withEntry (ResponseExtensions (null )),
512+ response: {
513+ "type" : "next" ,
514+ "data" : {"foo" : "bar" },
515+ "errors" : [
516+ {"message" : "error and data can coexist" }
517+ ]
518+ },
519+ ),
520+ ),
521+ );
522+ });
523+ test ('resubscribe after server disconnect' , () async {
524+ final payload = Request (
525+ operation: Operation (document: gql ('subscription {}' )),
526+ );
527+ final waitForConnection = true ;
528+ final subscriptionDataStream =
529+ socketClient.subscribe (payload, waitForConnection);
530+
531+ await expectLater (
532+ socketClient.connectionState,
533+ emitsInOrder ([
534+ SocketConnectionState .connecting,
535+ SocketConnectionState .handshake,
536+ SocketConnectionState .connected,
537+ ]),
538+ );
539+
540+ // We need to begin waiting on the connectionState
541+ // before we issue the command to disconnect; otherwise
542+ // it can reconnect so fast that it will be reconnected
543+ // by the time that the expectLater check is initiated.
544+ Timer (const Duration (milliseconds: 20 ), () async {
545+ socketClient.socketChannel! .sink.add (forceDisconnectCommand);
546+ });
547+ // The connectionState BehaviorController emits the current state
548+ // to any new listener, so we expect it to start in the connected
549+ // state, transition to notConnected, and then reconnect after that.
550+ await expectLater (
551+ socketClient.connectionState,
552+ emitsInOrder ([
553+ SocketConnectionState .connected,
554+ SocketConnectionState .notConnected,
555+ SocketConnectionState .connecting,
556+ SocketConnectionState .handshake,
557+ SocketConnectionState .connected,
558+ ]),
559+ );
560+
561+ // ignore: unawaited_futures
562+ socketClient.socketChannel! .stream
563+ .where ((message) => message == expectedMessage)
564+ .first
565+ .then ((_) {
566+ socketClient.socketChannel! .sink.add (jsonEncode ({
567+ 'type' : 'next' ,
568+ 'id' : '01020304-0506-4708-890a-0b0c0d0e0f10' ,
569+ 'payload' : {
570+ 'data' : {'foo' : 'bar' },
571+ 'errors' : [
572+ {'message' : 'error and data can coexist' }
573+ ]
574+ }
575+ }));
576+ });
577+
578+ await expectLater (
579+ subscriptionDataStream,
580+ emits (
581+ // todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
582+ Response (
583+ data: {'foo' : 'bar' },
584+ errors: [
585+ GraphQLError (message: 'error and data can coexist' ),
586+ ],
587+ context: Context ().withEntry (ResponseExtensions (null )),
588+ response: {
589+ "type" : "next" ,
590+ "data" : {"foo" : "bar" },
591+ "errors" : [
592+ {"message" : "error and data can coexist" }
593+ ]
594+ },
595+ ),
596+ ),
597+ );
598+ });
599+ }, tags: "integration" );
600+
327601 group ('SocketClient without autoReconnect' , () {
328602 late SocketClient socketClient;
329603 StreamController <dynamic > controller;
0 commit comments