zino-hofmann / graphql-flutter

A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package.
https://zino-hofmann.github.io/graphql-flutter
MIT License
3.25k stars 620 forks source link

fix(client): subscription hook return an uninitialized stream #1079

Closed PainteR closed 2 years ago

PainteR commented 2 years ago

Fixes / Enhancements

This PR fixes the useSubscription hook error LateInitializationError: Field 'stream' has not been initialized.

codecov[bot] commented 2 years ago

Codecov Report

Merging #1079 (724ec3e) into main (a093841) will increase coverage by 2.15%. The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main    #1079      +/-   ##
==========================================
+ Coverage   57.76%   59.92%   +2.15%     
==========================================
  Files          41       41              
  Lines        1520     1522       +2     
==========================================
+ Hits          878      912      +34     
+ Misses        642      610      -32     
Impacted Files Coverage Δ
...ql_flutter/lib/src/widgets/hooks/subscription.dart 58.69% <100.00%> (+58.69%) :arrow_up:
.../graphql_flutter/lib/src/widgets/subscription.dart 100.00% <0.00%> (+100.00%) :arrow_up:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update a093841...724ec3e. Read the comment docs.

budde377 commented 2 years ago

This was a pretty big bug, thanks for fixing!

Do you mind adding a test? I think the following should do the trick:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show MethodChannel, MethodCall;
import 'package:flutter_test/flutter_test.dart';

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';

class MockHttpClient extends Mock implements http.Client {
  @override
  Future<http.StreamedResponse> send(http.BaseRequest? request) =>
      super.noSuchMethod(
        Invocation.method(#send, [request]),
        returnValue: Future.value(
          http.StreamedResponse(
            Stream.fromIterable(const [<int>[]]),
            500,
          ),
        ),
      ) as Future<http.StreamedResponse>;
}

final query = gql("""
  subscription Foo {
    foo
  }
""");

/// https://flutter.dev/docs/cookbook/persistence/reading-writing-files#testing
Future<void> mockApplicationDocumentsDirectory() async {
  // Create a temporary directory.
  final directory = await Directory.systemTemp.createTemp();
  final handler = (MethodCall methodCall) async {
    // If you're getting the apps documents directory, return the path to the
    // temp directory on the test environment instead.
    if (methodCall.method == 'getApplicationDocumentsDirectory') {
      return directory.path;
    }
    return null;
  };
  // Mock out the MethodChannel for the path_provider plugin.
  const MethodChannel('plugins.flutter.io/path_provider')
      .setMockMethodCallHandler(handler);
  const MethodChannel('plugins.flutter.io/path_provider_macos')
      .setMockMethodCallHandler(handler);
}

class Page extends StatelessWidget {
  Page({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Subscription(
      options: SubscriptionOptions(
        document: query,
      ),
      builder: (QueryResult result, {Refetch? refetch, FetchMore? fetchMore}) =>
          Container(),
    );
  }
}

void main() {
  setUpAll(() async {
    await mockApplicationDocumentsDirectory();
    await initHiveForFlutter();
  });

  group('Subscription', () {
    late MockHttpClient mockHttpClient;
    HttpLink httpLink;
    ValueNotifier<GraphQLClient>? client;

    setUp(() async {
      mockHttpClient = MockHttpClient();
      httpLink = HttpLink(
        'https://unused/graphql',
        httpClient: mockHttpClient,
      );
      client = ValueNotifier(
        GraphQLClient(
          cache: GraphQLCache(store: await HiveStore.open()),
          link: httpLink,
        ),
      );
    });

    testWidgets('works', (WidgetTester tester) async {
      final page = Page();

      await tester.pumpWidget(GraphQLProvider(
        client: client,
        child: page,
      ));

      verify(
        mockHttpClient.send(
          argThat(isA<http.Request>()
              .having((request) => request.method, "method", "POST")
              .having((request) => request.headers, "headers", isNotNull)
              .having((request) => request.body, "body", isNotNull)
              .having(
                (request) => request.url,
                "expected endpoint",
                Uri.parse('https://unused/graphql'),
              )),
        ),
      ).called(1);
      await tester.pump();
      verifyNoMoreInteractions(mockHttpClient);
    });
  });
}