diff --git a/Changelog.md b/Changelog.md index 5607db011a..d6b8befd7c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ ### vNext +* Stricter type checking in the codebase. [#1617](https://github.com/apollographql/react-apollo/pull/1617) +* Improved TS types (even more) in both `Query` component and `graphql` HoC. [#1617](https://github.com/apollographql/react-apollo/pull/1617) * Fix React Component detection bug in `getDataFromTree` [#1604](https://github.com/apollographql/react-apollo/pull/1604) ### 2.1.0-beta.0 diff --git a/examples/typescript/src/schema.json b/examples/typescript/src/schema.json index 2401bbb768..b8919dcfc7 100644 --- a/examples/typescript/src/schema.json +++ b/examples/typescript/src/schema.json @@ -12,7 +12,8 @@ { "kind": "OBJECT", "name": "Query", - "description": "The query type, represents all of the entry points into our object graph", + "description": + "The query type, represents all of the entry points into our object graph", "fields": [ { "name": "hero", @@ -219,19 +220,22 @@ "enumValues": [ { "name": "NEWHOPE", - "description": "Star Wars Episode IV: A New Hope, released in 1977.", + "description": + "Star Wars Episode IV: A New Hope, released in 1977.", "isDeprecated": false, "deprecationReason": null }, { "name": "EMPIRE", - "description": "Star Wars Episode V: The Empire Strikes Back, released in 1980.", + "description": + "Star Wars Episode V: The Empire Strikes Back, released in 1980.", "isDeprecated": false, "deprecationReason": null }, { "name": "JEDI", - "description": "Star Wars Episode VI: Return of the Jedi, released in 1983.", + "description": + "Star Wars Episode VI: Return of the Jedi, released in 1983.", "isDeprecated": false, "deprecationReason": null } @@ -277,7 +281,8 @@ }, { "name": "friends", - "description": "The friends of the character, or an empty list if they have none", + "description": + "The friends of the character, or an empty list if they have none", "args": [], "type": { "kind": "LIST", @@ -293,7 +298,8 @@ }, { "name": "friendsConnection", - "description": "The friends of the character exposed as a connection with edges", + "description": + "The friends of the character exposed as a connection with edges", "args": [ { "name": "first", @@ -368,7 +374,8 @@ { "kind": "SCALAR", "name": "ID", - "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "description": + "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", "fields": null, "inputFields": null, "interfaces": null, @@ -378,7 +385,8 @@ { "kind": "SCALAR", "name": "String", - "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "description": + "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", "fields": null, "inputFields": null, "interfaces": null, @@ -388,7 +396,8 @@ { "kind": "SCALAR", "name": "Int", - "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "description": + "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", "fields": null, "inputFields": null, "interfaces": null, @@ -430,7 +439,8 @@ }, { "name": "friends", - "description": "A list of the friends, as a convenience when edges are not needed.", + "description": + "A list of the friends, as a convenience when edges are not needed.", "args": [], "type": { "kind": "LIST", @@ -489,7 +499,8 @@ }, { "name": "node", - "description": "The character represented by this friendship edge", + "description": + "The character represented by this friendship edge", "args": [], "type": { "kind": "INTERFACE", @@ -559,7 +570,8 @@ { "kind": "SCALAR", "name": "Boolean", - "description": "The `Boolean` scalar type represents `true` or `false`.", + "description": + "The `Boolean` scalar type represents `true` or `false`.", "fields": null, "inputFields": null, "interfaces": null, @@ -705,7 +717,8 @@ }, { "name": "friends", - "description": "This human's friends, or an empty list if they have none", + "description": + "This human's friends, or an empty list if they have none", "args": [], "type": { "kind": "LIST", @@ -721,7 +734,8 @@ }, { "name": "friendsConnection", - "description": "The friends of the human exposed as a connection with edges", + "description": + "The friends of the human exposed as a connection with edges", "args": [ { "name": "first", @@ -778,7 +792,8 @@ }, { "name": "starships", - "description": "A list of starships this person has piloted, or an empty list if none", + "description": + "A list of starships this person has piloted, or an empty list if none", "args": [], "type": { "kind": "LIST", @@ -830,7 +845,8 @@ { "kind": "SCALAR", "name": "Float", - "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", + "description": + "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", "fields": null, "inputFields": null, "interfaces": null, @@ -906,7 +922,8 @@ { "kind": "OBJECT", "name": "Droid", - "description": "An autonomous mechanical character in the Star Wars universe", + "description": + "An autonomous mechanical character in the Star Wars universe", "fields": [ { "name": "id", @@ -942,7 +959,8 @@ }, { "name": "friends", - "description": "This droid's friends, or an empty list if they have none", + "description": + "This droid's friends, or an empty list if they have none", "args": [], "type": { "kind": "LIST", @@ -958,7 +976,8 @@ }, { "name": "friendsConnection", - "description": "The friends of the droid exposed as a connection with edges", + "description": + "The friends of the droid exposed as a connection with edges", "args": [ { "name": "first", @@ -1040,7 +1059,8 @@ { "kind": "OBJECT", "name": "Mutation", - "description": "The mutation type, represents all updates we can make to our data", + "description": + "The mutation type, represents all updates we can make to our data", "fields": [ { "name": "createReview", @@ -1088,7 +1108,8 @@ { "kind": "INPUT_OBJECT", "name": "ReviewInput", - "description": "The input object sent when someone is creating a new review", + "description": + "The input object sent when someone is creating a new review", "fields": null, "inputFields": [ { @@ -1123,7 +1144,8 @@ { "kind": "OBJECT", "name": "__Schema", - "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "description": + "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", "fields": [ { "name": "types", @@ -1151,7 +1173,8 @@ }, { "name": "queryType", - "description": "The type that query operations will be rooted at.", + "description": + "The type that query operations will be rooted at.", "args": [], "type": { "kind": "NON_NULL", @@ -1167,7 +1190,8 @@ }, { "name": "mutationType", - "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "description": + "If this server supports mutation, the type that mutation operations will be rooted at.", "args": [], "type": { "kind": "OBJECT", @@ -1179,7 +1203,8 @@ }, { "name": "subscriptionType", - "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "description": + "If this server support subscription, the type that subscription operations will be rooted at.", "args": [], "type": { "kind": "OBJECT", @@ -1191,7 +1216,8 @@ }, { "name": "directives", - "description": "A list of all directives supported by this server.", + "description": + "A list of all directives supported by this server.", "args": [], "type": { "kind": "NON_NULL", @@ -1222,7 +1248,8 @@ { "kind": "OBJECT", "name": "__Type", - "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "description": + "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", "fields": [ { "name": "kind", @@ -1407,7 +1434,8 @@ { "kind": "ENUM", "name": "__TypeKind", - "description": "An enum describing what kind of type a given `__Type` is.", + "description": + "An enum describing what kind of type a given `__Type` is.", "fields": null, "inputFields": null, "interfaces": null, @@ -1420,43 +1448,50 @@ }, { "name": "OBJECT", - "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "description": + "Indicates this type is an object. `fields` and `interfaces` are valid fields.", "isDeprecated": false, "deprecationReason": null }, { "name": "INTERFACE", - "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "description": + "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", "isDeprecated": false, "deprecationReason": null }, { "name": "UNION", - "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "description": + "Indicates this type is a union. `possibleTypes` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "ENUM", - "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "description": + "Indicates this type is an enum. `enumValues` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "INPUT_OBJECT", - "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "description": + "Indicates this type is an input object. `inputFields` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "LIST", - "description": "Indicates this type is a list. `ofType` is a valid field.", + "description": + "Indicates this type is a list. `ofType` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "NON_NULL", - "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "description": + "Indicates this type is a non-null. `ofType` is a valid field.", "isDeprecated": false, "deprecationReason": null } @@ -1466,7 +1501,8 @@ { "kind": "OBJECT", "name": "__Field", - "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "description": + "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", "fields": [ { "name": "name", @@ -1573,7 +1609,8 @@ { "kind": "OBJECT", "name": "__InputValue", - "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "description": + "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", "fields": [ { "name": "name", @@ -1621,7 +1658,8 @@ }, { "name": "defaultValue", - "description": "A GraphQL-formatted string representing the default value for this input value.", + "description": + "A GraphQL-formatted string representing the default value for this input value.", "args": [], "type": { "kind": "SCALAR", @@ -1640,7 +1678,8 @@ { "kind": "OBJECT", "name": "__EnumValue", - "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "description": + "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", "fields": [ { "name": "name", @@ -1707,7 +1746,8 @@ { "kind": "OBJECT", "name": "__Directive", - "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "description": + "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", "fields": [ { "name": "name", @@ -1842,7 +1882,8 @@ { "kind": "ENUM", "name": "__DirectiveLocation", - "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "description": + "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", "fields": null, "inputFields": null, "interfaces": null, @@ -1945,13 +1986,15 @@ }, { "name": "INPUT_OBJECT", - "description": "Location adjacent to an input object type definition.", + "description": + "Location adjacent to an input object type definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "INPUT_FIELD_DEFINITION", - "description": "Location adjacent to an input object field definition.", + "description": + "Location adjacent to an input object field definition.", "isDeprecated": false, "deprecationReason": null } @@ -1962,12 +2005,9 @@ "directives": [ { "name": "skip", - "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], + "description": + "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", @@ -1987,12 +2027,9 @@ }, { "name": "include", - "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], + "description": + "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", @@ -2012,15 +2049,14 @@ }, { "name": "deprecated", - "description": "Marks an element of a GraphQL schema as no longer supported.", - "locations": [ - "FIELD_DEFINITION", - "ENUM_VALUE" - ], + "description": + "Marks an element of a GraphQL schema as no longer supported.", + "locations": ["FIELD_DEFINITION", "ENUM_VALUE"], "args": [ { "name": "reason", - "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", + "description": + "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", "type": { "kind": "SCALAR", "name": "String", @@ -2033,4 +2069,4 @@ ] } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 0e7db0eeb8..350f29026f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "bundlesize": [ { "path": "./lib/react-apollo.browser.umd.js", - "maxSize": "6 KB" + "maxSize": "6.5 KB" } ], "lint-staged": { @@ -84,13 +84,17 @@ }, "devDependencies": { "@types/enzyme": "3.1.8", + "@types/enzyme-adapter-react-16": "^1.0.1", "@types/graphql": "0.11.7", "@types/invariant": "2.2.29", "@types/jest": "22.0.1", "@types/lodash": "4.14.99", "@types/object-assign": "4.0.30", - "@types/react": "16.0.34", + "@types/prop-types": "15.5.2", + "@types/react": "16.0.35", "@types/react-dom": "16.0.3", + "@types/react-test-renderer": "16.0.0", + "@types/recompose": "0.24.4", "@types/zen-observable": "0.5.3", "apollo-cache-inmemory": "1.1.5", "apollo-client": "2.2.1", diff --git a/src/ApolloConsumer.tsx b/src/ApolloConsumer.tsx index 3b3e3f66c8..cad809a8e1 100644 --- a/src/ApolloConsumer.tsx +++ b/src/ApolloConsumer.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import ApolloClient from 'apollo-client'; -const invariant = require('invariant'); +import invariant from 'invariant'; export interface ApolloConsumerProps { - children: (client: ApolloClient) => React.ReactElement; + children: (client: ApolloClient) => React.ReactElement | null; } const ApolloConsumer: React.StatelessComponent = ( diff --git a/src/ApolloProvider.tsx b/src/ApolloProvider.tsx index 431a44128e..49f97046ad 100644 --- a/src/ApolloProvider.tsx +++ b/src/ApolloProvider.tsx @@ -3,8 +3,7 @@ import * as PropTypes from 'prop-types'; import { Component } from 'react'; import ApolloClient from 'apollo-client'; import QueryRecyclerProvider from './QueryRecyclerProvider'; - -const invariant = require('invariant'); +import invariant from 'invariant'; export interface ApolloProviderProps { client: ApolloClient; diff --git a/src/Query.tsx b/src/Query.tsx index 4b135f5281..5e6f4ca4ef 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -4,24 +4,62 @@ import ApolloClient, { ObservableQuery, ApolloQueryResult, ApolloError, - FetchMoreOptions, - UpdateQueryOptions, - FetchMoreQueryOptions, FetchPolicy, ApolloCurrentResult, + NetworkStatus, } from 'apollo-client'; import { DocumentNode } from 'graphql'; import { ZenObservable } from 'zen-observable-ts'; import { OperationVariables } from './types'; import { parser, DocumentType } from './parser'; +import pick from 'lodash/pick'; +import shallowEqual from 'fbjs/lib/shallowEqual'; +import invariant from 'invariant'; -const shallowEqual = require('fbjs/lib/shallowEqual'); -const invariant = require('invariant'); -const pick = require('lodash/pick'); +// Improved FetchMoreOptions type, need to port them back to Apollo Client +export interface FetchMoreOptions { + updateQuery: ( + previousQueryResult: TData, + options: { + fetchMoreResult?: TData; + variables: TVariables; + }, + ) => TData; +} -type ObservableQueryFields = Pick, 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling'>; +// Improved FetchMoreQueryOptions type, need to port them back to Apollo Client +export interface FetchMoreQueryOptions { + variables: Pick; +} + +// Improved ObservableQuery field types, need to port them back to Apollo Client +export type ObservableQueryFields = Pick< + ObservableQuery, + 'startPolling' | 'stopPolling' +> & { + refetch: (variables?: TVariables) => Promise>; + fetchMore: (( + fetchMoreOptions: FetchMoreQueryOptions & + FetchMoreOptions, + ) => Promise>) & + (( + fetchMoreOptions: { query: DocumentNode } & FetchMoreQueryOptions< + TVariables2, + K + > & + FetchMoreOptions, + ) => Promise>); + updateQuery: ( + mapFn: ( + previousQueryResult: TData, + options: { variables?: TVariables }, + ) => TData, + ) => void; +}; -function observableQueryFields(observable: ObservableQuery): ObservableQueryFields { +function observableQueryFields( + observable: ObservableQuery, +): ObservableQueryFields { const fields = pick( observable, 'refetch', @@ -32,33 +70,34 @@ function observableQueryFields(observable: ObservableQuery): Obser ); Object.keys(fields).forEach(key => { - if (typeof fields[key] === 'function') { - fields[key] = fields[key].bind(observable); + const k = key as + | 'refetch' + | 'fetchMore' + | 'updateQuery' + | 'startPolling' + | 'stopPolling'; + if (typeof fields[k] === 'function') { + fields[k] = fields[k].bind(observable); } }); - return fields; + // TODO: Need to cast this because we improved the type of `updateQuery` to be parametric + // on variables, while the type in Apollo client just has object. + // Consider removing this when that is properly typed + return fields as ObservableQueryFields; } function isDataFilled(data: {} | TData): data is TData { return Object.keys(data).length > 0; } -export interface QueryResult { +export interface QueryResult + extends ObservableQueryFields { client: ApolloClient; data?: TData; error?: ApolloError; - fetchMore: ( - fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions, - ) => Promise>; loading: boolean; - networkStatus: number; - refetch: (variables?: TVariables) => Promise>; - startPolling: (pollInterval: number) => void; - stopPolling: () => void; - updateQuery: ( - mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any, - ) => void; + networkStatus: NetworkStatus; } export interface QueryProps { @@ -75,20 +114,24 @@ export interface QueryState { result: ApolloCurrentResult; } -class Query extends React.Component< - QueryProps, - QueryState -> { +export interface QueryContext { + client: ApolloClient; +} + +class Query< + TData = any, + TVariables = OperationVariables +> extends React.Component, QueryState> { static contextTypes = { client: PropTypes.object.isRequired, }; + context: QueryContext; - state: QueryState; - private client: ApolloClient; + private client: ApolloClient; private queryObservable: ObservableQuery; private querySubscription: ZenObservable.Subscription; - constructor(props: QueryProps, context: any) { + constructor(props: QueryProps, context: QueryContext) { super(props, context); invariant( @@ -130,7 +173,10 @@ class Query extends React.Componen this.startQuerySubscription(); } - componentWillReceiveProps(nextProps, nextContext) { + componentWillReceiveProps( + nextProps: QueryProps, + nextContext: QueryContext, + ) { if ( shallowEqual(this.props, nextProps) && this.client === nextContext.client @@ -157,7 +203,9 @@ class Query extends React.Componen return children(queryResult); } - private initializeQueryObservable = props => { + private initializeQueryObservable = ( + props: QueryProps, + ) => { const { variables, pollInterval, diff --git a/src/Subscriptions.tsx b/src/Subscriptions.tsx index 1944c67c54..32dadfafe3 100644 --- a/src/Subscriptions.tsx +++ b/src/Subscriptions.tsx @@ -13,13 +13,16 @@ const invariant = require('invariant'); export interface SubscriptionResult { loading: boolean; data?: TData; - error: ApolloError; + error?: ApolloError; } -export interface SubscriptionProps { +export interface SubscriptionProps< + TData = any, + TVariables = OperationVariables +> { query: DocumentNode; - variables?: OperationVariables; - children: (result: any) => React.ReactNode; + variables?: TVariables; + children: (result: SubscriptionResult) => React.ReactNode; } export interface SubscriptionState { @@ -28,8 +31,12 @@ export interface SubscriptionState { error?: ApolloError; } -class Subscription extends React.Component< - SubscriptionProps, +export interface SubscriptionContext { + client: ApolloClient; +} + +class Subscription extends React.Component< + SubscriptionProps, SubscriptionState > { static contextTypes = { @@ -40,7 +47,10 @@ class Subscription extends React.Component< private queryObservable: Observable; private querySubscription: ZenObservable.Subscription; - constructor(props: SubscriptionProps, context: any) { + constructor( + props: SubscriptionProps, + context: SubscriptionContext, + ) { super(props, context); invariant( @@ -56,7 +66,10 @@ class Subscription extends React.Component< this.startSubscription(); } - componentWillReceiveProps(nextProps, nextContext) { + componentWillReceiveProps( + nextProps: SubscriptionProps, + nextContext: SubscriptionContext, + ) { if ( shallowEqual(this.props, nextProps) && this.client === nextContext.client @@ -88,7 +101,7 @@ class Subscription extends React.Component< return this.props.children(result); } - private initialize = props => { + private initialize = (props: SubscriptionProps) => { this.queryObservable = this.client.subscribe({ query: props.query, variables: props.variables, @@ -110,7 +123,7 @@ class Subscription extends React.Component< }; }; - private updateCurrentData = result => { + private updateCurrentData = (result: SubscriptionResult) => { this.setState({ data: result.data, loading: false, @@ -118,7 +131,7 @@ class Subscription extends React.Component< }); }; - private updateError = error => { + private updateError = (error: any) => { this.setState({ error, loading: false, diff --git a/src/getDataFromTree.ts b/src/getDataFromTree.ts index 6dbbe800fa..319eb12ebf 100755 --- a/src/getDataFromTree.ts +++ b/src/getDataFromTree.ts @@ -1,6 +1,14 @@ -import { Children, ReactElement, StatelessComponent } from 'react'; -import ApolloClient, { ApolloQueryResult } from 'apollo-client'; -const assign = require('object-assign'); +import { + Children, + ReactElement, + ReactNode, + Component, + ComponentType, + ComponentClass, + ChildContextProvider, +} from 'react'; +import ApolloClient from 'apollo-client'; +import assign from 'object-assign'; export interface Context { client?: ApolloClient; @@ -14,31 +22,51 @@ export interface QueryTreeArgument { } export interface QueryTreeResult { - query: Promise>; + query: Promise; element: ReactElement; context: Context; } -interface PreactElement { - attributes: any; +interface PreactElement

{ + attributes: P; } -function getProps(element: ReactElement | PreactElement): any { +function getProps

(element: ReactElement

| PreactElement

): P { return ( - (element as ReactElement).props || - (element as PreactElement).attributes + (element as ReactElement

).props || + (element as PreactElement

).attributes ); } +function isReactElement( + element: string | number | true | {} | ReactElement | React.ReactPortal, +): element is ReactElement { + return !!(element as any).type; +} + +function isComponentClass( + Comp: ComponentType, +): Comp is ComponentClass { + return ( + Comp.prototype && (Comp.prototype.render || Comp.prototype.isReactComponent) + ); +} + +function providesChildContext( + instance: Component, +): instance is Component & ChildContextProvider { + return !!(instance as any).getChildContext; +} + // Recurse a React Element tree, running visitor on each element. // If visitor returns `false`, don't call the element's render function // or recurse into its child elements export function walkTree( - element: ReactElement | any, + element: ReactNode, context: Context, visitor: ( - element: ReactElement, - instance: any, + element: ReactElement | string | number, + instance: Component | null, context: Context, ) => boolean | void, ) { @@ -50,97 +78,117 @@ export function walkTree( if (!element) return; - const Component = element.type; // a stateless functional component or a class - if (typeof Component === 'function') { - const props = assign({}, Component.defaultProps, getProps(element)); - let childContext = context; - let child; - - // Are we are a react class? - // https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L66 - if (Component.prototype && (Component.prototype.render || Component.prototype.isReactComponent)) { - // typescript force casting since typescript doesn't have definitions for class - // methods - const _component = Component as any; - const instance = new _component(props, context); - // In case the user doesn't pass these to super in the constructor - instance.props = instance.props || instance.attributes || props; - instance.context = instance.context || context; - // set the instance state to null (not undefined) if not set, to match React behaviour - instance.state = instance.state || null; - - // Override setState to just change the state, not queue up an update. - // (we can't do the default React thing as we aren't mounted "properly" - // however, we don't need to re-render as well only support setState in - // componentWillMount, which happens *before* render). - instance.setState = newState => { - if (typeof newState === 'function') { - newState = newState(instance.state, instance.props, instance.context); + if (isReactElement(element)) { + if (typeof element.type === 'function') { + const Comp = element.type; + const props = assign({}, Comp.defaultProps, getProps(element)); + let childContext = context; + let child; + + // Are we are a react class? + // https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L66 + if (isComponentClass(Comp)) { + const instance = new Comp(props, context); + // In case the user doesn't pass these to super in the constructor + instance.props = instance.props || props; + instance.context = instance.context || context; + // set the instance state to null (not undefined) if not set, to match React behaviour + instance.state = instance.state || null; + + // Override setState to just change the state, not queue up an update. + // (we can't do the default React thing as we aren't mounted "properly" + // however, we don't need to re-render as well only support setState in + // componentWillMount, which happens *before* render). + instance.setState = newState => { + if (typeof newState === 'function') { + // React's TS type definitions don't contain context as a third parameter for + // setState's updater function. + // Remove this cast to `any` when that is fixed. + newState = (newState as any)( + instance.state, + instance.props, + instance.context, + ); + } + instance.state = assign({}, instance.state, newState); + }; + + // this is a poor man's version of + // https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L181 + if (instance.componentWillMount) { + instance.componentWillMount(); } - instance.state = assign({}, instance.state, newState); - }; - // this is a poor man's version of - // https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L181 - if (instance.componentWillMount) { - instance.componentWillMount(); - } + if (providesChildContext(instance)) { + childContext = assign({}, context, instance.getChildContext()); + } - if (instance.getChildContext) { - childContext = assign({}, context, instance.getChildContext()); - } + if (visitor(element, instance, context) === false) { + return; + } - if (visitor(element, instance, context) === false) { - return; + child = instance.render(); + } else { + // just a stateless functional + if (visitor(element, null, context) === false) { + return; + } + + child = Comp(props, context); } - child = instance.render(); + if (child) { + if (Array.isArray(child)) { + child.forEach(item => walkTree(item, context, visitor)); + } else { + walkTree(child, childContext, visitor); + } + } } else { - // just a stateless functional + // a basic string or dom element, just get children if (visitor(element, null, context) === false) { return; } - // typescript casting for stateless component - const _component = Component as StatelessComponent; - child = _component(props, context); - } - - if (child) { - if (Array.isArray(child)) { - child.forEach(item => walkTree(item, context, visitor)); - } else { - walkTree(child, childContext, visitor); + if (element.props && element.props.children) { + Children.forEach(element.props.children, (child: any) => { + if (child) { + walkTree(child, context, visitor); + } + }); } } - } else { - // a basic string or dom element, just get children - if (visitor(element, null, context) === false) { - return; - } - - if (element.props && element.props.children) { - Children.forEach(element.props.children, (child: any) => { - if (child) { - walkTree(child, context, visitor); - } - }); - } + } else if (typeof element === 'string' || typeof element === 'number') { + // Just visit these, they are leaves so we don't keep traversing. + visitor(element, null, context); } + // TODO: Portals? +} + +function hasFetchDataFunction( + instance: Component, +): instance is Component & { fetchData: () => Object } { + return typeof (instance as any).fetchData === 'function'; +} + +function isPromise(query: Object): query is Promise { + return typeof (query as any).then === 'function'; } function getQueriesFromTree( { rootElement, rootContext = {} }: QueryTreeArgument, fetchRoot: boolean = true, ): QueryTreeResult[] { - const queries = []; + const queries: QueryTreeResult[] = []; walkTree(rootElement, rootContext, (element, instance, context) => { const skipRoot = !fetchRoot && element === rootElement; - if (instance && typeof instance.fetchData === 'function' && !skipRoot) { + if (skipRoot) return; + + if (instance && isReactElement(element) && hasFetchDataFunction(instance)) { const query = instance.fetchData(); - if (query) { + if (isPromise(query)) { queries.push({ query, element, context }); // Tell walkTree to not recurse inside this component; we will @@ -164,7 +212,7 @@ export default function getDataFromTree( // no queries found, nothing to do if (!queries.length) return Promise.resolve(); - const errors = []; + const errors: any[] = []; // wait on each query that we found, re-rendering the subtree when it's done const mappedQueries = queries.map(({ query, element, context }) => { // we've just grabbed the query for element, so don't try and get it again diff --git a/src/graphql.tsx b/src/graphql.tsx index f9b6483297..66a17adc51 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -9,26 +9,34 @@ import { parser, DocumentType } from './parser'; import { DocumentNode } from 'graphql'; import { MutationOpts, - ChildProps, OperationOption, QueryOpts, GraphqlQueryControls, MutationFunc, OptionProps, + DataProps, + MutateProps, } from './types'; - -const shallowEqual = require('fbjs/lib/shallowEqual'); -const invariant = require('invariant'); -const assign = require('object-assign'); -const pick = require('lodash/pick'); -const hoistNonReactStatics = require('hoist-non-react-statics'); +import { OperationVariables } from './index'; +import pick from 'lodash/pick'; +import assign from 'object-assign'; +import hoistNonReactStatics from 'hoist-non-react-statics'; +import shallowEqual from 'fbjs/lib/shallowEqual'; +import invariant from 'invariant'; const defaultMapPropsToOptions = () => ({}); -const defaultMapResultToProps = props => props; +const defaultMapResultToProps:

(props: P) => P = props => props; const defaultMapPropsToSkip = () => false; +type ObservableQueryFields = Pick< + ObservableQuery, + 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling' +>; + // the fields we want to copy over to our data prop -function observableQueryFields(observable) { +function observableQueryFields( + observable: ObservableQuery, +): ObservableQueryFields { const fields = pick( observable, 'variables', @@ -41,15 +49,21 @@ function observableQueryFields(observable) { ); Object.keys(fields).forEach(key => { - if (typeof fields[key] === 'function') { - fields[key] = fields[key].bind(observable); + const k = key as + | 'refetch' + | 'fetchMore' + | 'updateQuery' + | 'startPolling' + | 'stopPolling'; + if (typeof fields[k] === 'function') { + fields[k] = fields[k].bind(observable); } }); return fields; } -function getDisplayName(WrappedComponent) { +function getDisplayName

(WrappedComponent: React.ComponentType

) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } @@ -57,13 +71,19 @@ function getDisplayName(WrappedComponent) { let nextVersion = 0; export default function graphql< - TProps = {}, + TProps extends TGraphQLVariables | {} = {}, TData = {}, TGraphQLVariables = {}, - TChildProps = ChildProps + TChildProps = Partial> & + Partial> >( document: DocumentNode, - operationOptions: OperationOption = {}, + operationOptions: OperationOption< + TProps, + TData, + TGraphQLVariables, + TChildProps + > = {}, ) { // extract options const { @@ -87,13 +107,19 @@ export default function graphql< // Helps track hot reloading. const version = nextVersion++; - function wrapWithApolloComponent( - WrappedComponent: React.ComponentType, - ) { + function wrapWithApolloComponent( + WrappedComponent: React.ComponentType, + ): React.ComponentClass { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; - type GraphqlProps = TOriginalProps & TGraphQLVariables; - class GraphQL extends React.Component { + type GraphqlProps = TProps; + + interface GraphqlContext { + client: ApolloClient; + getQueryRecycler: () => void; + } + + class GraphQL extends React.Component { static displayName = graphQLDisplayName; static WrappedComponent = WrappedComponent; static contextTypes = { @@ -102,7 +128,6 @@ export default function graphql< }; // react / redux and react dev tools (HMR) needs - public props: any; // passed props public version: number; public hasMounted: boolean; @@ -119,7 +144,7 @@ export default function graphql< private lastSubscriptionData: any; private refetcherQueue: { args: any; - resolve: (value?: {} | PromiseLike<{}>) => void; + resolve: (value?: any | PromiseLike) => void; reject: (reason?: any) => void; }; @@ -132,7 +157,7 @@ export default function graphql< // wrapped instance private wrappedInstance: any; - constructor(props: GraphqlProps, context: any) { + constructor(props: GraphqlProps, context: GraphqlContext) { super(props, context); this.version = version; @@ -164,7 +189,10 @@ export default function graphql< } } - componentWillReceiveProps(nextProps, nextContext) { + componentWillReceiveProps( + nextProps: GraphqlProps, + nextContext: GraphqlContext, + ) { if (this.shouldSkip(nextProps)) { if (!this.shouldSkip(this.props)) { // if this has changed, we better unsubscribe @@ -251,7 +279,7 @@ export default function graphql< ); } - getClient(props): ApolloClient { + getClient(props: GraphqlProps): ApolloClient { if (this.client) return this.client; const { client } = mapPropsToOptions(props); @@ -271,7 +299,7 @@ export default function graphql< return this.client; } - calculateOptions(props = this.props, newOpts?) { + calculateOptions(props = this.props, newOpts?: QueryOpts | MutationOpts) { let opts = mapPropsToOptions(props); if (newOpts && newOpts.variables) { @@ -281,23 +309,26 @@ export default function graphql< if (opts.variables || !operation.variables.length) return opts; - let variables = {}; + let variables: OperationVariables = {}; for (let { variable, type } of operation.variables) { if (!variable.name || !variable.name.value) continue; - if (typeof props[variable.name.value] !== 'undefined') { - variables[variable.name.value] = props[variable.name.value]; + const variableName = variable.name.value; + const variableProp = (props as any)[variableName]; + + if (typeof variableProp !== 'undefined') { + variables[variableName] = variableProp; continue; } // allow optional props if (type.kind !== 'NonNullType') { - variables[variable.name.value] = null; + variables[variableName] = null; continue; } invariant( - typeof props[variable.name.value] !== 'undefined', + typeof variableProp !== 'undefined', `The operation '${operation.name}' wrapping '${getDisplayName( WrappedComponent, )}' ` + @@ -317,7 +348,7 @@ export default function graphql< let name = this.type === DocumentType.Mutation ? 'mutate' : 'data'; if (operationOptions.name) name = operationOptions.name; - const newResult: OptionProps = { + const newResult: OptionProps = { [name]: result, ownProps: this.props, }; @@ -371,7 +402,7 @@ export default function graphql< } } - updateQuery(props) { + updateQuery(props: Readonly) { const opts = this.calculateOptions(props) as QueryOpts; // if we skipped initially, we may not have yet created the observable @@ -450,7 +481,7 @@ export default function graphql< this.forceRenderChildren(); }; - const handleError = error => { + const handleError = (error: any) => { this.resubscribeToQuery(); // Quick fix for https://github.com/apollostack/react-apollo/issues/378 if (error.hasOwnProperty('graphQLErrors')) return next({ error }); @@ -515,11 +546,11 @@ export default function graphql< return this.wrappedInstance; } - setWrappedInstance(ref) { + setWrappedInstance(ref: React.ComponentClass) { this.wrappedInstance = ref; } - dataForChildViaMutation(mutationOpts: MutationOpts) { + dataForChildViaMutation(mutationOpts?: MutationOpts) { const opts = this.calculateOptions(this.props, mutationOpts); if (typeof opts.variables === 'undefined') delete opts.variables; @@ -612,12 +643,13 @@ export default function graphql< render() { if (this.shouldSkip()) { if (operationOptions.withRef) { - return React.createElement( - WrappedComponent, - assign({}, this.props, { ref: this.setWrappedInstance }), + return ( + ); } - return React.createElement(WrappedComponent, this.props); + return ; } const { shouldRerender, renderedElement, props } = this; @@ -636,11 +668,8 @@ export default function graphql< const mergedPropsAndData = assign({}, props, clientProps); if (operationOptions.withRef) - mergedPropsAndData.ref = this.setWrappedInstance; - this.renderedElement = React.createElement( - WrappedComponent, - mergedPropsAndData, - ); + (mergedPropsAndData as any).ref = this.setWrappedInstance; + this.renderedElement = ; return this.renderedElement; } } diff --git a/src/parser.ts b/src/parser.ts index 05adfbca2a..c3319c27f0 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -5,7 +5,7 @@ import { OperationDefinitionNode, } from 'graphql'; -const invariant = require('invariant'); +import invariant from 'invariant'; export enum DocumentType { Query, @@ -89,7 +89,12 @@ export function parser(document: DocumentNode): IDocumentDefinition { const definition = definitions[0] as OperationDefinitionNode; variables = definition.variableDefinitions || []; - let hasName = definition.name && definition.name.kind === 'Name'; - name = hasName ? definition.name.value : 'data'; // fallback to using data if no name + + if (definition.name && definition.name.kind === 'Name') { + name = definition.name.value; + } else { + name = 'data'; // fallback to using data if no name + } + return { name, type, variables }; } diff --git a/src/test-links.ts b/src/test-links.ts index 7223b06a87..a8f1f87ab7 100644 --- a/src/test-links.ts +++ b/src/test-links.ts @@ -100,7 +100,7 @@ export class MockSubscriptionLink extends ApolloLink { super(); } - public request(req: any) { + public request(_req: any) { return new Observable(observer => { this.setups.forEach(x => x()); this.observer = observer; diff --git a/src/types.ts b/src/types.ts index 6e23fd19c9..fb1eb562f1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,9 +15,12 @@ export type OperationVariables = { [key: string]: any; }; -export interface MutationOpts { +export interface MutationOpts< + TData = any, + TGraphQLVariables = OperationVariables +> { variables?: TGraphQLVariables; - optimisticResponse?: Object; + optimisticResponse?: TData; refetchQueries?: string[] | PureQueryOptions[]; update?: MutationUpdaterFn; client?: ApolloClient; @@ -55,7 +58,7 @@ export type MutationFunc< TData = any, TGraphQLVariables = OperationVariables > = ( - opts: MutationOpts, + opts?: MutationOpts, ) => Promise>; export type DataValue< @@ -115,21 +118,21 @@ export interface OptionProps< export interface OperationOption< TProps, TData, - TGraphQLVariables = OperationVariables + TGraphQLVariables = OperationVariables, + TChildProps = ChildProps > { options?: | QueryOpts - | MutationOpts + | MutationOpts | (( props: TProps, - ) => QueryOpts | MutationOpts); - props?: (props: OptionProps) => any; + ) => + | QueryOpts + | MutationOpts); + props?: (props: OptionProps) => TChildProps; skip?: boolean | ((props: any) => boolean); name?: string; withRef?: boolean; - shouldResubscribe?: ( - props: TProps & DataProps, - nextProps: TProps & DataProps, - ) => boolean; + shouldResubscribe?: (props: TProps, nextProps: TProps) => boolean; alias?: string; } diff --git a/src/withApollo.tsx b/src/withApollo.tsx index b1d1de9bac..90d3bdc735 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -1,29 +1,31 @@ import * as React from 'react'; import { OperationOption } from './types'; import ApolloConsumer from './ApolloConsumer'; +import { ApolloClient } from 'apollo-client'; +import assign from 'object-assign'; +import invariant from 'invariant'; +import hoistNonReactStatics from 'hoist-non-react-statics'; -const invariant = require('invariant'); - -const hoistNonReactStatics = require('hoist-non-react-statics'); - -function getDisplayName(WrappedComponent) { +function getDisplayName

(WrappedComponent: React.ComponentType

) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } +export type WithApolloClient

= P & { client: ApolloClient }; + export default function withApollo( - WrappedComponent, + WrappedComponent: React.ComponentType>, operationOptions: OperationOption = {}, -) { +): React.ComponentClass { const withDisplayName = `withApollo(${getDisplayName(WrappedComponent)})`; - class WithApollo extends React.Component { + class WithApollo extends React.Component { static displayName = withDisplayName; static WrappedComponent = WrappedComponent; // wrapped instance private wrappedInstance: any; - constructor(props) { + constructor(props: TProps) { super(props); this.setWrappedInstance = this.setWrappedInstance.bind(this); } @@ -38,22 +40,22 @@ export default function withApollo( return this.wrappedInstance; } - setWrappedInstance(ref) { + setWrappedInstance(ref: React.ComponentType>) { this.wrappedInstance = ref; } render() { return ( - {client => ( - - )} + {client => { + const props = assign({}, this.props, { + client, + ref: operationOptions.withRef + ? this.setWrappedInstance + : undefined, + }); + return ; + }} ); } diff --git a/test/client/ApolloConsumer.test.tsx b/test/client/ApolloConsumer.test.tsx index ca96eef89a..efed5ec88e 100644 --- a/test/client/ApolloConsumer.test.tsx +++ b/test/client/ApolloConsumer.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; const client = new ApolloClient({ cache: new Cache(), - link: new ApolloLink((o, f) => f(o)), + link: new ApolloLink((o, f) => (f ? f(o) : null)), }); describe(' component', () => { @@ -33,7 +33,7 @@ describe(' component', () => { it('renders the content in the children prop', () => { const wrapper = mount( - {clientRender =>

} + {() =>
} , ); diff --git a/test/client/ApolloProvider.test.tsx b/test/client/ApolloProvider.test.tsx index 69f6034f14..28144c49d1 100644 --- a/test/client/ApolloProvider.test.tsx +++ b/test/client/ApolloProvider.test.tsx @@ -10,7 +10,7 @@ import ApolloProvider from '../../src/ApolloProvider'; describe(' Component', () => { const client = new ApolloClient({ cache: new Cache(), - link: new ApolloLink((o, f) => f(o)), + link: new ApolloLink((o, f) => (f ? f(o) : null)), }); interface ChildContext { @@ -33,8 +33,12 @@ describe(' Component', () => { } } - class Container extends React.Component { - constructor(props) { + interface Props { + client: ApolloClient; + } + + class Container extends React.Component { + constructor(props: Props) { super(props); this.state = {}; } @@ -111,7 +115,7 @@ describe(' Component', () => { }; expect(() => { shallow( - +
, ); @@ -162,7 +166,7 @@ describe(' Component', () => { const newClient = new ApolloClient({ cache: new Cache(), - link: new ApolloLink((o, f) => f(o)), + link: new ApolloLink((o, f) => (f ? f(o) : null)), }); container.setState({ client: newClient }); expect(container.find(ApolloProvider).props().client).toEqual(newClient); @@ -179,7 +183,7 @@ describe(' Component', () => { const newClient = new ApolloClient({ cache: new Cache(), - link: new ApolloLink((o, f) => f(o)), + link: new ApolloLink((o, f) => (f ? f(o) : null)), }); container.setState({ client: newClient }); diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index 3a50781310..0ec3338c8f 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; -import ApolloClient from 'apollo-client'; +import ApolloClient, { NetworkStatus } from 'apollo-client'; import gql from 'graphql-tag'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import ApolloProvider from '../../src/ApolloProvider'; import Query from '../../src/Query'; import { MockedProvider, mockSingleLink } from '../../src/test-utils'; import catchAsyncError from '../test-utils/catchAsyncError'; import stripSymbols from '../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; -const allPeopleQuery = gql` +const allPeopleQuery: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -18,7 +19,16 @@ const allPeopleQuery = gql` } } `; -const allPeopleData = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + +interface Data { + allPeople: { + people: Array<{ name: string }>; + }; +} + +const allPeopleData: Data = { + allPeople: { people: [{ name: 'Luke Skywalker' }] }, +}; const allPeopleMocks = [ { request: { query: allPeopleQuery }, @@ -26,8 +36,10 @@ const allPeopleMocks = [ }, ]; +class AllPeopleQuery extends Query {} + describe('Query component', () => { - let wrapper; + let wrapper: ReactWrapper | null; beforeEach(() => { jest.useRealTimers(); }); @@ -80,7 +92,7 @@ describe('Query component', () => { it('renders using the children prop', done => { const Component = () => ( - {result =>
} + {_ =>
} ); wrapper = mount( @@ -89,14 +101,14 @@ describe('Query component', () => { , ); catchAsyncError(done, () => { - expect(wrapper.find('div').exists()).toBeTruthy(); + expect(wrapper!.find('div').exists()).toBeTruthy(); done(); }); }); describe('result provides', () => { it('client', done => { - const queryWithVariables = gql` + const queryWithVariables: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -174,7 +186,7 @@ describe('Query component', () => { }); it('refetch', done => { - const queryRefetch = gql` + const queryRefetch: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -213,7 +225,7 @@ describe('Query component', () => { expect.assertions(5); const Component = () => ( - { return null; }} - + ); wrapper = mount( @@ -295,7 +307,7 @@ describe('Query component', () => { expect.assertions(2); const Component = () => ( - + {result => { if (result.loading) { return null; @@ -304,14 +316,17 @@ describe('Query component', () => { result .fetchMore({ variables: { first: 1 }, - updateQuery: (prev, { fetchMoreResult }) => ({ - allPeople: { - people: [ - ...prev.allPeople.people, - ...fetchMoreResult.allPeople.people, - ], - }, - }), + updateQuery: (prev, { fetchMoreResult }) => + fetchMoreResult + ? { + allPeople: { + people: [ + ...prev.allPeople.people, + ...fetchMoreResult.allPeople.people, + ], + }, + } + : prev, }) .then(result2 => { expect(stripSymbols(result2.data)).toEqual(data2); @@ -335,7 +350,7 @@ describe('Query component', () => { count++; return null; }} - + ); wrapper = mount( @@ -486,10 +501,11 @@ describe('Query component', () => { }, ]; - let isUpdated; + let isUpdated = false; expect.assertions(3); + const Component = () => ( - + {result => { if (result.loading) { return null; @@ -517,7 +533,7 @@ describe('Query component', () => { return null; }} - + ); wrapper = mount( @@ -536,7 +552,7 @@ describe('Query component', () => { catchAsyncError(done, () => { expect(result.loading).toBeFalsy(); expect(result.data).toBeUndefined(); - expect(result.networkStatus).toBe(7); + expect(result.networkStatus).toBe(NetworkStatus.ready); done(); }); return null; @@ -765,7 +781,7 @@ describe('Query component', () => { const { variables } = this.state; return ( - + {result => { if (result.loading) { return null; @@ -785,7 +801,7 @@ describe('Query component', () => { count++; return null; }} - + ); } } @@ -950,7 +966,7 @@ describe('Query component', () => { class Component extends React.Component { state = { query: allPeopleQuery }; - componentDidCatch(error) { + componentDidCatch(error: any) { catchAsyncError(done, () => { const expectedError = new Error( 'The component requires a graphql query, but got a subscription.', @@ -984,7 +1000,7 @@ describe('Query component', () => { }); it('should be able to refetch after there was a network error', done => { - const query = gql` + const query: DocumentNode = gql` query somethingelse { allPeople(first: 1) { people { @@ -1009,10 +1025,12 @@ describe('Query component', () => { let count = 0; const noop = () => null; + class AllPeopleQuery2 extends Query {} + function Container() { return ( - - {(result) => { + + {result => { try { switch (count++) { case 0: @@ -1020,17 +1038,19 @@ describe('Query component', () => { expect(result.loading).toBeTruthy(); break; case 1: + if (!result.data) { + done.fail('Should have data by this point'); + break; + } // First result is loaded, run a refetch to get the second result // which is an error. expect(stripSymbols(result.data.allPeople)).toEqual( data.allPeople, ); setTimeout(() => { - result - .refetch() - .then((val) => { - done.fail('Expected error value on first refetch.'); - }, noop); + result.refetch().then(() => { + done.fail('Expected error value on first refetch.'); + }, noop); }, 0); break; case 2: @@ -1043,11 +1063,9 @@ describe('Query component', () => { expect(result.loading).toBeFalsy(); expect(result.error).toBeTruthy(); setTimeout(() => { - result - .refetch() - .catch(() => { - done.fail('Expected good data on second refetch.'); - }); + result.refetch().catch(() => { + done.fail('Expected good data on second refetch.'); + }); }, 0); break; // Further fix required in QueryManager, we should have an extra @@ -1060,6 +1078,10 @@ describe('Query component', () => { // Third result's data is loaded expect(result.loading).toBeFalsy(); expect(result.error).toBeFalsy(); + if (!result.data) { + done.fail('Should have data by this point'); + break; + } expect(stripSymbols(result.data.allPeople)).toEqual( dataTwo.allPeople, ); @@ -1073,8 +1095,8 @@ describe('Query component', () => { } return null; }} - - ) + + ); } wrapper = mount( diff --git a/test/client/Subscription.test.tsx b/test/client/Subscription.test.tsx index 1c1001299b..75633d56d2 100644 --- a/test/client/Subscription.test.tsx +++ b/test/client/Subscription.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import gql from 'graphql-tag'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { ApolloClient } from 'apollo-client'; -import { ApolloLink } from 'apollo-link'; +import { ApolloLink, Operation } from 'apollo-link'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { MockSubscriptionLink } from '../../src/test-utils'; @@ -20,7 +20,7 @@ const results = [ result: { data: { user: { name } } }, })); -let wrapper; +let wrapper: ReactWrapper | null; beforeEach(() => { jest.useRealTimers(); @@ -116,7 +116,7 @@ it('executes subscription for the variables passed in the props', done => { const variables = { name: 'Luke Skywalker' }; class MockSubscriptionLinkOverride extends MockSubscriptionLink { - request(req) { + request(req: Operation) { catchAsyncError(done, () => { expect(req.variables).toEqual(variables); }); @@ -294,7 +294,7 @@ describe('should update', () => { const userLink = new MockSubscriptionLink(); const heroLink = new MockSubscriptionLink(); - const linkCombined = new ApolloLink((o, f) => f(o)).split( + const linkCombined = new ApolloLink((o, f) => (f ? f(o) : null)).split( ({ operationName }) => operationName === 'HeroInfo', heroLink, userLink, @@ -386,7 +386,7 @@ describe('should update', () => { class MockSubscriptionLinkOverride extends MockSubscriptionLink { variables: any; - request(req) { + request(req: Operation) { this.variables = req.variables; return super.request(req); } diff --git a/test/client/__snapshots__/Query.test.tsx.snap b/test/client/__snapshots__/Query.test.tsx.snap index f9c6b600da..df21779b67 100644 --- a/test/client/__snapshots__/Query.test.tsx.snap +++ b/test/client/__snapshots__/Query.test.tsx.snap @@ -25,7 +25,9 @@ Object { } `; -exports[`Query component calls the children prop: result in render prop while loading 1`] = ` +exports[ + `Query component calls the children prop: result in render prop while loading 1` +] = ` Object { "data": undefined, "error": undefined, diff --git a/test/client/graphql/client-option.test.tsx b/test/client/graphql/client-option.test.tsx index e83108cc56..688d45c4ac 100644 --- a/test/client/graphql/client-option.test.tsx +++ b/test/client/graphql/client-option.test.tsx @@ -5,14 +5,15 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../src'; import { ObservableQueryRecycler } from '../../../src/queryRecycler'; import stripSymbols from '../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('client option', () => { it('renders with client from options', () => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -22,6 +23,9 @@ describe('client option', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -35,7 +39,7 @@ describe('client option', () => { client, }, }; - const ContainerWithData = graphql(query, config)(props => null); + const ContainerWithData = graphql<{}, Data>(query, config)(() => null); shallow(, { context: { getQueryRecycler: () => new ObservableQueryRecycler() }, }); @@ -51,6 +55,8 @@ describe('client option', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -64,12 +70,12 @@ describe('client option', () => { client, }, }; - const ContainerWithData = graphql(query, config)(props => null); + const ContainerWithData = graphql<{}, Data>(query, config)(() => null); shallow(); }); it('ignores client from context if client from options is present', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -81,6 +87,8 @@ describe('client option', () => { const dataProvider = { allPeople: { people: [{ name: 'Leia Organa Solo' }] }, }; + + type Data = typeof dataProvider; const linkProvider = mockSingleLink({ request: { query }, result: { data: dataProvider }, @@ -105,11 +113,10 @@ describe('client option', () => { }, }; - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // tslint:disable-line - expect(data.loading).toBeFalsy(); // first data - expect(stripSymbols(data.allPeople)).toEqual({ + class Container extends React.Component> { + componentWillReceiveProps({ data }: ChildProps<{}, Data>) { + expect(data!.loading).toBeFalsy(); // first data + expect(stripSymbols(data!.allPeople)).toEqual({ people: [{ name: 'Luke Skywalker' }], }); done(); @@ -118,7 +125,7 @@ describe('client option', () => { return null; } } - const ContainerWithData = graphql(query, config)(Container); + const ContainerWithData = graphql<{}, Data>(query, config)(Container); renderer.create( @@ -126,7 +133,7 @@ describe('client option', () => { ); }); it('exposes refetch as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -136,7 +143,11 @@ describe('client option', () => { } `; const variables = { first: 1 }; + type Variables = typeof variables; + const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; + const link = mockSingleLink({ request: { query, variables }, result: { data: data1 }, @@ -146,17 +157,19 @@ describe('client option', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // tslint:disable-line - expect(data.loading).toBeFalsy(); // first data - done(); - } - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component> { + componentWillReceiveProps({ + data, + }: ChildProps) { + expect(data!.loading).toBeFalsy(); // first data + done(); + } + render() { + return null; + } + }, + ); renderer.create( diff --git a/test/client/graphql/fragments.test.tsx b/test/client/graphql/fragments.test.tsx index c53ef51b25..988b058d87 100644 --- a/test/client/graphql/fragments.test.tsx +++ b/test/client/graphql/fragments.test.tsx @@ -4,14 +4,15 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../src'; import stripSymbols from '../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('fragments', () => { // XXX in a later version, we should support this for composition it('throws if you only pass a fragment', () => { - const query = gql` + const query: DocumentNode = gql` fragment Failure on PeopleConnection { people { name @@ -21,6 +22,8 @@ describe('fragments', () => { const expectedData = { allPeople: { people: [{ name: 'Luke Skywalker' }] }, }; + type Data = typeof expectedData; + const link = mockSingleLink({ request: { query }, result: { data: expectedData }, @@ -31,18 +34,19 @@ describe('fragments', () => { }); try { - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual( - expectedData.allPeople, - ); - } - render() { - return null; - } - } + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual( + expectedData.allPeople, + ); + } + render() { + return null; + } + }, + ); renderer.create( @@ -56,7 +60,7 @@ describe('fragments', () => { }); it('correctly fetches a query with inline fragments', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { __typename @@ -76,6 +80,9 @@ describe('fragments', () => { people: [{ name: 'Luke Skywalker' }], }, }; + + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -85,17 +92,18 @@ describe('fragments', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - done(); - } - render() { - return null; - } - } + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + done(); + } + render() { + return null; + } + }, + ); renderer.create( diff --git a/test/client/graphql/mutations/index.test.tsx b/test/client/graphql/mutations/index.test.tsx index 1c8eeb4985..bf9acd8136 100644 --- a/test/client/graphql/mutations/index.test.tsx +++ b/test/client/graphql/mutations/index.test.tsx @@ -1,17 +1,14 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; import gql from 'graphql-tag'; -import { - ApolloProvider, - ChildProps, - graphql, - MutateProps, - MutationFunc, -} from '../../../../src'; +import { ApolloProvider, ChildProps, graphql } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; import createClient from '../../../test-utils/createClient'; +import { NormalizedCacheObject } from 'apollo-cache-inmemory'; +import { ApolloClient } from 'apollo-client'; +import { DocumentNode } from 'graphql'; -const query = gql` +const query: DocumentNode = gql` mutation addPerson { allPeople(first: 1) { people { @@ -20,13 +17,24 @@ const query = gql` } } `; + +interface Data { + allPeople: { + people: { name: string }[]; + }; +} + +interface Variables { + name: string; +} + const expectedData = { allPeople: { people: [{ name: 'Luke Skywalker' }] }, }; describe('graphql(mutation)', () => { - let error; - let client; + let error: typeof console.error; + let client: ApolloClient; beforeEach(() => { error = console.error; console.error = jest.fn(() => {}); // tslint:disable-line @@ -37,7 +45,7 @@ describe('graphql(mutation)', () => { }); it('binds a mutation to props', () => { - const ContainerWithData = graphql(query)(({ mutate }: MutateProps) => { + const ContainerWithData = graphql(query)(({ mutate }) => { expect(mutate).toBeTruthy(); expect(typeof mutate).toBe('function'); return null; @@ -54,20 +62,22 @@ describe('graphql(mutation)', () => { interface Props { methodName: string; } - const ContainerWithData = graphql(query, { - props: ({ ownProps, mutate: addPerson }) => ({ - [ownProps.methodName]: (name: string) => - addPerson({ variables: { name } }), - }), - })( - ({ - myInjectedMutationMethod, - }: ChildProps & { myInjectedMutationMethod: MutationFunc }) => { - expect(test).toBeTruthy(); - expect(typeof test).toBe('function'); - return null; + type InjectedProps = { + [name: string]: (name: string) => void; + }; + const ContainerWithData = graphql( + query, + { + props: ({ ownProps, mutate: addPerson }) => ({ + [ownProps.methodName]: (name: string) => + addPerson!({ variables: { name } }), + }), }, - ); + )(({ myInjectedMutationMethod }) => { + expect(myInjectedMutationMethod).toBeTruthy(); + expect(typeof myInjectedMutationMethod).toBe('function'); + return null; + }); renderer.create( @@ -77,14 +87,14 @@ describe('graphql(mutation)', () => { }); it('does not swallow children errors', done => { - let bar; + let bar: any; const ContainerWithData = graphql(query)(() => { bar(); // this will throw return null; }); class ErrorBoundary extends React.Component { - componentDidCatch(e, info) { + componentDidCatch(e: Error) { expect(e.name).toMatch(/TypeError/); expect(e.message).toMatch(/bar is not a function/); done(); @@ -105,18 +115,19 @@ describe('graphql(mutation)', () => { }); it('can execute a mutation', done => { - @graphql(query) - class Container extends React.Component { - componentDidMount() { - this.props.mutate().then(result => { - expect(stripSymbols(result.data)).toEqual(expectedData); - done(); - }); - } - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component { + componentDidMount() { + this.props.mutate!().then(result => { + expect(stripSymbols(result.data)).toEqual(expectedData); + done(); + }); + } + render() { + return null; + } + }, + ); renderer.create( @@ -136,19 +147,25 @@ describe('graphql(mutation)', () => { } `; client = createClient(expectedData, queryWithVariables, { first: 1 }); - @graphql(queryWithVariables) - class Container extends React.Component { - componentDidMount() { - this.props.mutate().then(result => { - expect(stripSymbols(result.data)).toEqual(expectedData); - done(); - }); - } - render() { - return null; - } + + interface Props { + first: number; } + const Container = graphql(queryWithVariables)( + class extends React.Component> { + componentDidMount() { + this.props.mutate!().then(result => { + expect(stripSymbols(result.data)).toEqual(expectedData); + done(); + }); + } + render() { + return null; + } + }, + ); + renderer.create( diff --git a/test/client/graphql/mutations/lifecycle.test.tsx b/test/client/graphql/mutations/lifecycle.test.tsx index b7a56abfc8..0cdc3583d8 100644 --- a/test/client/graphql/mutations/lifecycle.test.tsx +++ b/test/client/graphql/mutations/lifecycle.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; import gql from 'graphql-tag'; -import { ApolloProvider, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; import createClient from '../../../test-utils/createClient'; @@ -22,20 +22,25 @@ describe('graphql(mutation) lifecycle', () => { it('allows falsy values in the mapped variables from props', done => { const client = createClient(expectedData, query, { id: null }); - @graphql(query) - class Container extends React.Component { - componentDidMount() { - this.props.mutate().then(result => { - expect(stripSymbols(result.data)).toEqual(expectedData); - done(); - }); - } - - render() { - return null; - } + interface Props { + id: string | null; } + const Container = graphql(query)( + class extends React.Component> { + componentDidMount() { + this.props.mutate!().then(result => { + expect(stripSymbols(result.data)).toEqual(expectedData); + done(); + }); + } + + render() { + return null; + } + }, + ); + renderer.create( @@ -45,7 +50,10 @@ describe('graphql(mutation) lifecycle', () => { it("errors if the passed props don't contain the needed variables", () => { const client = createClient(expectedData, query, { first: 1 }); - const Container = graphql(query)(() => null); + interface Props { + frst: number; + } + const Container = graphql(query)(() => null); try { renderer.create( @@ -59,27 +67,33 @@ describe('graphql(mutation) lifecycle', () => { it('rebuilds the mutation on prop change when using `options`', done => { const client = createClient(expectedData, query, { - id: null, + id: 2, }); - function options(props) { + + interface Props { + listId: number; + } + function options(props: Props) { return { variables: { - id: null, + id: props.listId, }, }; } - @graphql(query, { options }) - class Container extends React.Component { - componentWillReceiveProps(props) { + class Container extends React.Component> { + componentWillReceiveProps(props: ChildProps) { if (props.listId !== 2) return; - props.mutate().then(x => done()); + props.mutate!().then(() => done()); } render() { return null; } } - class ChangingProps extends React.Component { + + const ContainerWithMutate = graphql(query, { options })(Container); + + class ChangingProps extends React.Component<{}, { listId: number }> { state = { listId: 1 }; componentDidMount() { @@ -87,7 +101,7 @@ describe('graphql(mutation) lifecycle', () => { } render() { - return ; + return ; } } @@ -100,19 +114,24 @@ describe('graphql(mutation) lifecycle', () => { it('can execute a mutation with custom variables', done => { const client = createClient(expectedData, query, { id: 1 }); - @graphql(query) - class Container extends React.Component { - componentDidMount() { - this.props.mutate({ variables: { id: 1 } }).then(result => { - expect(stripSymbols(result.data)).toEqual(expectedData); - done(); - }); - } - render() { - return null; - } + interface Variables { + id: number; } + const Container = graphql<{}, {}, Variables>(query)( + class extends React.Component> { + componentDidMount() { + this.props.mutate!({ variables: { id: 1 } }).then(result => { + expect(stripSymbols(result.data)).toEqual(expectedData); + done(); + }); + } + render() { + return null; + } + }, + ); + renderer.create( diff --git a/test/client/graphql/mutations/queries.test.tsx b/test/client/graphql/mutations/queries.test.tsx index 0a45f614fb..02b0372d15 100644 --- a/test/client/graphql/mutations/queries.test.tsx +++ b/test/client/graphql/mutations/queries.test.tsx @@ -1,23 +1,17 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; import gql from 'graphql-tag'; -import ApolloClient from 'apollo-client'; +import ApolloClient, { MutationUpdaterFn } from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { - ApolloProvider, - graphql, - ChildProps, - MutationFunc, -} from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; import createClient from '../../../test-utils/createClient'; - -const compose = require('lodash/flowRight'); +import { DocumentNode } from 'graphql'; describe('graphql(mutation) query integration', () => { it('allows for passing optimisticResponse for a mutation', done => { - const query = gql` + const query: DocumentNode = gql` mutation createTodo { createTodo { id @@ -38,33 +32,37 @@ describe('graphql(mutation) query integration', () => { completed: true, }, }; + + type Data = typeof data; + const client = createClient(data, query); - @graphql(query) - class Container extends React.Component { - componentDidMount() { - const optimisticResponse = { - __typename: 'Mutation', - createTodo: { - __typename: 'Todo', - id: '99', - text: 'Optimistically generated', - completed: true, - }, - }; - this.props.mutate({ optimisticResponse }).then(result => { - expect(stripSymbols(result.data)).toEqual(data); - done(); - }); + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentDidMount() { + const optimisticResponse = { + __typename: 'Mutation', + createTodo: { + __typename: 'Todo', + id: '99', + text: 'Optimistically generated', + completed: true, + }, + }; + this.props.mutate!({ optimisticResponse }).then(result => { + expect(stripSymbols(result.data)).toEqual(data); + done(); + }); - const dataInStore = client.cache.extract(true); - expect(stripSymbols(dataInStore['Todo:99'])).toEqual( - optimisticResponse.createTodo, - ); - } - render() { - return null; - } - } + const dataInStore = client.cache.extract(true); + expect(stripSymbols(dataInStore['Todo:99'])).toEqual( + optimisticResponse.createTodo, + ); + } + render() { + return null; + } + }, + ); renderer.create( @@ -74,7 +72,7 @@ describe('graphql(mutation) query integration', () => { }); it('allows for updating queries from a mutation', done => { - const query = gql` + const query: DocumentNode = gql` query todos { todo_list { id @@ -88,7 +86,7 @@ describe('graphql(mutation) query integration', () => { } `; - const mutation = gql` + const mutation: DocumentNode = gql` mutation createTodo { createTodo { id @@ -106,6 +104,8 @@ describe('graphql(mutation) query integration', () => { }, }; + type MutationData = typeof mutationData; + const optimisticResponse = { createTodo: { id: '99', @@ -113,10 +113,17 @@ describe('graphql(mutation) query integration', () => { completed: true, }, }; + interface QueryData { + todo_list: { + id: string; + title: string; + tasks: { id: string; text: string; completed: boolean }[]; + }; + } - const update = (proxy, { data: { createTodo } }) => { - const data = proxy.readQuery({ query }); // read from cache - data.todo_list.tasks.push(createTodo); // update value + const update: MutationUpdaterFn = (proxy, result) => { + const data = proxy.readQuery({ query }); // read from cache + data!.todo_list.tasks.push(result.data!.createTodo); // update value proxy.writeQuery({ query, data }); // write to cache }; @@ -134,16 +141,21 @@ describe('graphql(mutation) query integration', () => { const cache = new Cache({ addTypename: false }); const client = new ApolloClient({ link, cache }); - let count = 0; - @graphql(query) - @graphql(mutation, { + const withQuery = graphql<{}, QueryData>(query); + + type WithQueryChildProps = ChildProps<{}, QueryData>; + const withMutation = graphql(mutation, { options: () => ({ optimisticResponse, update }), - }) - class Container extends React.Component { - componentWillReceiveProps(props) { - if (!props.data.todo_list) return; + }); + + let count = 0; + + type ContainerProps = ChildProps; + class Container extends React.Component { + componentWillReceiveProps(props: ContainerProps) { + if (!props.data || !props.data.todo_list) return; if (!props.data.todo_list.tasks.length) { - props.mutate().then(result => { + props.mutate!().then(result => { expect(stripSymbols(result.data)).toEqual(mutationData); }); @@ -171,14 +183,16 @@ describe('graphql(mutation) query integration', () => { } } + const ContainerWithData = withQuery(withMutation(Container)); + renderer.create( - + , ); }); it('allows for updating queries from a mutation automatically', done => { - const query = gql` + const query: DocumentNode = gql` query getMini($id: ID!) { mini(id: $id) { __typename @@ -196,9 +210,13 @@ describe('graphql(mutation) query integration', () => { }, }; + type Data = typeof queryData; + const variables = { id: 1 }; - const mutation = gql` + type Variables = typeof variables; + + const mutation: DocumentNode = gql` mutation($signature: String!) { mini: submitMiniCoverS3DirectUpload(signature: $signature) { __typename @@ -216,6 +234,12 @@ describe('graphql(mutation) query integration', () => { }, }; + type MutationData = typeof mutationData; + + interface MutationVariables { + signature: string; + } + const link = mockSingleLink( { request: { query, variables }, result: { data: queryData } }, { @@ -227,7 +251,7 @@ describe('graphql(mutation) query integration', () => { const client = new ApolloClient({ link, cache }); class Boundary extends React.Component { - componentDidCatch(e) { + componentDidCatch(e: any) { done.fail(e); } render() { @@ -236,36 +260,46 @@ describe('graphql(mutation) query integration', () => { } let count = 0; - @graphql(mutation) - class MutationContainer extends React.Component { - componentWillReceiveProps(props) { - if (count === 1) { - props.mutate().then(result => { - expect(stripSymbols(result.data)).toEqual(mutationData); - }); + const MutationContainer = graphql( + mutation, + )( + class extends React.Component< + ChildProps + > { + componentWillReceiveProps( + props: ChildProps, + ) { + if (count === 1) { + props.mutate!().then(result => { + expect(stripSymbols(result.data)).toEqual(mutationData); + }); + } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - if (count === 0) { - expect(stripSymbols(props.data.mini)).toEqual(queryData.mini); + const Container = graphql(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps) { + if (count === 0) { + expect(stripSymbols(props.data!.mini)).toEqual(queryData.mini); + } + if (count === 1) { + expect(stripSymbols(props.data!.mini)).toEqual(mutationData.mini); + done(); + } + count++; } - if (count === 1) { - expect(stripSymbols(props.data.mini)).toEqual(mutationData.mini); - done(); + render() { + return ( + + ); } - count++; - } - render() { - return ; - } - } + }, + ); renderer.create( @@ -280,7 +314,7 @@ describe('graphql(mutation) query integration', () => { // reproduction of query from Apollo Engine const accountId = '1234'; - const billingInfoQuery = gql` + const billingInfoQuery: DocumentNode = gql` query Account__PaymentDetailQuery($accountId: ID!) { account(id: $accountId) { id @@ -292,7 +326,7 @@ describe('graphql(mutation) query integration', () => { } `; - const overlappingQuery = gql` + const overlappingQuery: DocumentNode = gql` query Account__PaymentQuery($accountId: ID!) { account(id: $accountId) { id @@ -323,7 +357,13 @@ describe('graphql(mutation) query integration', () => { }, }; - const setPlanMutation = gql` + type QueryData = typeof data1; + + interface QueryVariables { + accountId: string; + } + + const setPlanMutation: DocumentNode = gql` mutation Account__SetPlanMutation($accountId: ID!, $planId: ID!) { accountSetPlan(accountId: $accountId, planId: $planId) } @@ -333,6 +373,13 @@ describe('graphql(mutation) query integration', () => { accountSetPlan: true, }; + type MutationData = typeof mutationData; + + interface MutationVariables { + accountId: string; + planId: string; + } + const variables = { accountId, }; @@ -362,7 +409,7 @@ describe('graphql(mutation) query integration', () => { const client = new ApolloClient({ link, cache }); class Boundary extends React.Component { - componentDidCatch(e) { + componentDidCatch(e: any) { done.fail(e); } render() { @@ -370,11 +417,15 @@ describe('graphql(mutation) query integration', () => { } } - let refetched; - class RelatedUIComponent extends React.Component { - componentWillReceiveProps(props) { + let refetched = false; + class RelatedUIComponent extends React.Component< + ChildProps<{}, QueryData, QueryVariables> + > { + componentWillReceiveProps( + props: ChildProps<{}, QueryData, QueryVariables>, + ) { if (refetched) { - expect(props.billingData.account.currentPlan.name).toBe('Free'); + expect(props.data!.account!.currentPlan.name).toBe('Free'); done(); } } @@ -383,18 +434,34 @@ describe('graphql(mutation) query integration', () => { } } - const RelatedUIComponentWithData = graphql(overlappingQuery, { + const RelatedUIComponentWithData = graphql<{}, QueryData, QueryVariables>( + overlappingQuery, + { + options: { variables: { accountId } }, + }, + )(RelatedUIComponent); + + const withQuery = graphql<{}, QueryData, QueryVariables>(billingInfoQuery, { options: { variables: { accountId } }, - name: 'billingData', - })(RelatedUIComponent); + }); + type WithQueryChildProps = ChildProps<{}, QueryData, QueryVariables>; + + const withMutation = graphql< + WithQueryChildProps, + MutationData, + MutationVariables + >(setPlanMutation); + type WithMutationChildProps = ChildProps< + WithQueryChildProps, + MutationData, + MutationVariables + >; let count = 0; - class PaymentDetail extends React.Component< - ChildProps & { setPlan: MutationFunc } - > { - componentWillReceiveProps(props) { + class PaymentDetail extends React.Component { + componentWillReceiveProps(props: WithMutationChildProps) { if (count === 1) { - expect(props.billingData.account.currentPlan.name).toBe('Free'); + expect(props.data!.account!.currentPlan.name).toBe('Free'); done(); } count++; @@ -402,7 +469,7 @@ describe('graphql(mutation) query integration', () => { async onPaymentInfoChanged() { try { refetched = true; - await this.props.setPlan({ + await this.props.mutate!({ refetchQueries: [ { query: billingInfoQuery, @@ -433,17 +500,7 @@ describe('graphql(mutation) query integration', () => { } } - const PaymentDetailWithData = compose( - graphql(setPlanMutation, { - name: 'setPlan', - }), - graphql(billingInfoQuery, { - options: () => ({ - variables: { accountId }, - }), - name: 'billingData', - }), - )(PaymentDetail); + const PaymentDetailWithData = withQuery(withMutation(PaymentDetail)); renderer.create( diff --git a/test/client/graphql/mutations/recycled-queries.test.tsx b/test/client/graphql/mutations/recycled-queries.test.tsx index 7ee91ecb0c..0d911d29f4 100644 --- a/test/client/graphql/mutations/recycled-queries.test.tsx +++ b/test/client/graphql/mutations/recycled-queries.test.tsx @@ -1,11 +1,17 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; import gql from 'graphql-tag'; -import ApolloClient from 'apollo-client'; +import ApolloClient, { MutationUpdaterFn } from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../../src'; +import { + ApolloProvider, + graphql, + ChildProps, + MutationFunc, +} from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('graphql(mutation) update queries', () => { // This is a long test that keeps track of a lot of stuff. It is testing @@ -29,7 +35,7 @@ describe('graphql(mutation) update queries', () => { // going as smoothly as planned. it('will run `update` for a previously mounted component', () => new Promise((resolve, reject) => { - const query = gql` + const query: DocumentNode = gql` query todos { todo_list { id @@ -43,7 +49,15 @@ describe('graphql(mutation) update queries', () => { } `; - const mutation = gql` + interface QueryData { + todo_list: { + id: string; + title: string; + tasks: { id: string; text: string; completed: boolean }[]; + }; + } + + const mutation: DocumentNode = gql` mutation createTodo { createTodo { id @@ -60,12 +74,13 @@ describe('graphql(mutation) update queries', () => { completed: true, }, }; + type MutationData = typeof mutationData; let todoUpdateQueryCount = 0; - const update = (proxy, { data: { createTodo } }) => { + const update: MutationUpdaterFn = (proxy, result) => { todoUpdateQueryCount++; - const data = proxy.readQuery({ query }); // read from cache - data.todo_list.tasks.push(createTodo); // update value + const data = proxy.readQuery({ query }); // read from cache + data!.todo_list.tasks.push(result.data!.createTodo); // update value proxy.writeQuery({ query, data }); // write to cache }; @@ -86,109 +101,113 @@ describe('graphql(mutation) update queries', () => { cache: new Cache({ addTypename: false }), }); - let mutate; + let mutate: MutationFunc; - @graphql(mutation, { options: () => ({ update }) }) - class MyMutation extends React.Component { - componentDidMount() { - mutate = this.props.mutate; - } + const MyMutation = graphql<{}, MutationData>(mutation, { + options: () => ({ update }), + })( + class extends React.Component> { + componentDidMount() { + mutate = this.props.mutate!; + } - render() { - return null; - } - } + render() { + return null; + } + }, + ); let queryMountCount = 0; let queryUnmountCount = 0; let queryRenderCount = 0; - @graphql(query) - class MyQuery extends React.Component { - componentWillMount() { - queryMountCount++; - } + const MyQuery = graphql<{}, QueryData>(query)( + class extends React.Component> { + componentWillMount() { + queryMountCount++; + } - componentWillUnmount() { - queryUnmountCount++; - } + componentWillUnmount() { + queryUnmountCount++; + } - render() { - try { - switch (queryRenderCount++) { - case 0: - expect(this.props.data.loading).toBeTruthy(); - expect(this.props.data.todo_list).toBeFalsy(); - break; - case 1: - expect(stripSymbols(this.props.data.todo_list)).toEqual({ - id: '123', - title: 'how to apollo', - tasks: [], - }); - break; - case 2: - expect(queryMountCount).toBe(1); - expect(queryUnmountCount).toBe(0); - expect(stripSymbols(this.props.data.todo_list)).toEqual({ - id: '123', - title: 'how to apollo', - tasks: [ - { - id: '99', - text: 'This one was created with a mutation.', - completed: true, - }, - ], - }); - break; - case 3: - expect(queryMountCount).toBe(2); - expect(queryUnmountCount).toBe(1); - expect(stripSymbols(this.props.data.todo_list)).toEqual({ - id: '123', - title: 'how to apollo', - tasks: [ - { - id: '99', - text: 'This one was created with a mutation.', - completed: true, - }, - { - id: '99', - text: 'This one was created with a mutation.', - completed: true, - }, - ], - }); - break; - case 4: - expect(stripSymbols(this.props.data.todo_list)).toEqual({ - id: '123', - title: 'how to apollo', - tasks: [ - { - id: '99', - text: 'This one was created with a mutation.', - completed: true, - }, - { - id: '99', - text: 'This one was created with a mutation.', - completed: true, - }, - ], - }); - break; - default: - throw new Error('Rendered too many times'); + render() { + try { + switch (queryRenderCount++) { + case 0: + expect(this.props.data!.loading).toBeTruthy(); + expect(this.props.data!.todo_list).toBeFalsy(); + break; + case 1: + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ + id: '123', + title: 'how to apollo', + tasks: [], + }); + break; + case 2: + expect(queryMountCount).toBe(1); + expect(queryUnmountCount).toBe(0); + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ + id: '123', + title: 'how to apollo', + tasks: [ + { + id: '99', + text: 'This one was created with a mutation.', + completed: true, + }, + ], + }); + break; + case 3: + expect(queryMountCount).toBe(2); + expect(queryUnmountCount).toBe(1); + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ + id: '123', + title: 'how to apollo', + tasks: [ + { + id: '99', + text: 'This one was created with a mutation.', + completed: true, + }, + { + id: '99', + text: 'This one was created with a mutation.', + completed: true, + }, + ], + }); + break; + case 4: + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ + id: '123', + title: 'how to apollo', + tasks: [ + { + id: '99', + text: 'This one was created with a mutation.', + completed: true, + }, + { + id: '99', + text: 'This one was created with a mutation.', + completed: true, + }, + ], + }); + break; + default: + throw new Error('Rendered too many times'); + } + } catch (error) { + reject(error); } - } catch (error) { - reject(error); + return null; } - return null; - } - } + }, + ); const wrapperMutation = renderer.create( @@ -221,7 +240,7 @@ describe('graphql(mutation) update queries', () => { setTimeout(() => { const wrapperQuery2 = renderer.create( - + , ); @@ -248,7 +267,7 @@ describe('graphql(mutation) update queries', () => { it('will run `refetchQueries` for a recycled queries', () => new Promise((resolve, reject) => { - const mutation = gql` + const mutation: DocumentNode = gql` mutation createTodo { createTodo { id @@ -266,7 +285,9 @@ describe('graphql(mutation) update queries', () => { }, }; - const query = gql` + type MutationData = typeof mutationData; + + const query: DocumentNode = gql` query todos($id: ID!) { todo_list(id: $id) { id @@ -280,6 +301,18 @@ describe('graphql(mutation) update queries', () => { } `; + interface QueryData { + todo_list: { + id: string; + title: string; + tasks: { id: string; text: string; completed: boolean }[]; + }; + } + + interface QueryVariables { + id: string; + } + const data = { todo_list: { id: '123', title: 'how to apollo', tasks: [] }, }; @@ -305,71 +338,75 @@ describe('graphql(mutation) update queries', () => { cache: new Cache({ addTypename: false }), }); - let mutate; + let mutate: MutationFunc; - @graphql(mutation, {}) - class Mutation extends React.Component { - componentDidMount() { - mutate = this.props.mutate; - } + const Mutation = graphql<{}, MutationData>(mutation)( + class extends React.Component> { + componentDidMount() { + mutate = this.props.mutate!; + } - render() { - return null; - } - } + render() { + return null; + } + }, + ); let queryMountCount = 0; let queryUnmountCount = 0; let queryRenderCount = 0; - @graphql(query) - class Query extends React.Component { - componentWillMount() { - queryMountCount++; - } + const Query = graphql(query)( + class extends React.Component< + ChildProps + > { + componentWillMount() { + queryMountCount++; + } - componentWillUnmount() { - queryUnmountCount++; - } + componentWillUnmount() { + queryUnmountCount++; + } - render() { - try { - switch (queryRenderCount++) { - case 0: - expect(this.props.data.loading).toBeTruthy(); - expect(this.props.data.todo_list).toBeFalsy(); - break; - case 1: - expect(this.props.data.loading).toBeFalsy(); - expect(stripSymbols(this.props.data.todo_list)).toEqual({ - id: '123', - title: 'how to apollo', - tasks: [], - }); - break; - case 2: - expect(queryMountCount).toBe(2); - expect(queryUnmountCount).toBe(1); - expect(stripSymbols(this.props.data.todo_list)).toEqual( - updatedData.todo_list, - ); - break; - case 3: - expect(queryMountCount).toBe(2); - expect(queryUnmountCount).toBe(1); - expect(stripSymbols(this.props.data.todo_list)).toEqual( - updatedData.todo_list, - ); - break; - default: - throw new Error('Rendered too many times'); + render() { + try { + switch (queryRenderCount++) { + case 0: + expect(this.props.data!.loading).toBeTruthy(); + expect(this.props.data!.todo_list).toBeFalsy(); + break; + case 1: + expect(this.props.data!.loading).toBeFalsy(); + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ + id: '123', + title: 'how to apollo', + tasks: [], + }); + break; + case 2: + expect(queryMountCount).toBe(2); + expect(queryUnmountCount).toBe(1); + expect(stripSymbols(this.props.data!.todo_list)).toEqual( + updatedData.todo_list, + ); + break; + case 3: + expect(queryMountCount).toBe(2); + expect(queryUnmountCount).toBe(1); + expect(stripSymbols(this.props.data!.todo_list)).toEqual( + updatedData.todo_list, + ); + break; + default: + throw new Error('Rendered too many times'); + } + } catch (error) { + reject(error); } - } catch (error) { - reject(error); + return null; } - return null; - } - } + }, + ); renderer.create( @@ -387,7 +424,7 @@ describe('graphql(mutation) update queries', () => { wrapperQuery1.unmount(); mutate({ refetchQueries: ['todos'] }) - .then((...args) => { + .then(() => { setTimeout(() => { // This re-renders the recycled query that should have been refetched while recycled. renderer.create( diff --git a/test/client/graphql/queries/api.test.tsx b/test/client/graphql/queries/api.test.tsx index 8d86cfb56d..f43f512c79 100644 --- a/test/client/graphql/queries/api.test.tsx +++ b/test/client/graphql/queries/api.test.tsx @@ -4,20 +4,16 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { - ApolloProvider, - ChildProps, - graphql, - OptionProps, -} from '../../../../src'; +import { ApolloProvider, ChildProps, graphql } from '../../../../src'; import wrap from '../../../test-utils/wrap'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('[queries] api', () => { // api it('exposes refetch as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -38,7 +34,7 @@ describe('[queries] api', () => { cache: new Cache({ addTypename: false }), }); - let hasRefetched, + let hasRefetched = false, count = 0; interface Props { @@ -48,45 +44,48 @@ describe('[queries] api', () => { allPeople: { people: [{ name: string }] }; } - @graphql(query) - class Container extends React.Component> { - componentWillMount() { - expect(this.props.data.refetch).toBeTruthy(); - expect(this.props.data.refetch instanceof Function).toBeTruthy(); - } - componentWillReceiveProps({ data }: ChildProps) { - try { - if (count === 0) expect(data.loading).toBeFalsy(); // first data - if (count === 1) expect(data.loading).toBeTruthy(); // first refetch - if (count === 2) expect(data.loading).toBeFalsy(); // second data - if (count === 3) expect(data.loading).toBeTruthy(); // second refetch - if (count === 4) expect(data.loading).toBeFalsy(); // third data - count++; - if (hasRefetched) return; - hasRefetched = true; - expect(data.refetch).toBeTruthy(); - expect(data.refetch instanceof Function).toBeTruthy(); - data - .refetch() - .then(result => { - expect(stripSymbols(result.data)).toEqual(data1); - return data - .refetch({ first: 2 }) // new variables - .then(response => { - expect(stripSymbols(response.data)).toEqual(data1); - expect(stripSymbols(data.allPeople)).toEqual(data1.allPeople); - done(); - }); - }) - .catch(done.fail); - } catch (e) { - done.fail(e); + const Container = graphql(query)( + class extends React.Component> { + componentWillMount() { + expect(this.props.data!.refetch).toBeTruthy(); + expect(this.props.data!.refetch instanceof Function).toBeTruthy(); } - } - render() { - return
{this.props.first}
; - } - } + componentWillReceiveProps({ data }: ChildProps) { + try { + if (count === 0) expect(data!.loading).toBeFalsy(); // first data + if (count === 1) expect(data!.loading).toBeTruthy(); // first refetch + if (count === 2) expect(data!.loading).toBeFalsy(); // second data + if (count === 3) expect(data!.loading).toBeTruthy(); // second refetch + if (count === 4) expect(data!.loading).toBeFalsy(); // third data + count++; + if (hasRefetched) return; + hasRefetched = true; + expect(data!.refetch).toBeTruthy(); + expect(data!.refetch instanceof Function).toBeTruthy(); + data! + .refetch() + .then(result => { + expect(stripSymbols(result.data)).toEqual(data1); + return data! + .refetch({ first: 2 }) // new variables + .then(response => { + expect(stripSymbols(response.data)).toEqual(data1); + expect(stripSymbols(data!.allPeople)).toEqual( + data1.allPeople, + ); + done(); + }); + }) + .catch(done.fail); + } catch (e) { + done.fail(e); + } + } + render() { + return
{this.props.first}
; + } + }, + ); renderer.create( @@ -96,7 +95,7 @@ describe('[queries] api', () => { }); it('exposes subscribeToMore as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -115,18 +114,19 @@ describe('[queries] api', () => { }); // example of loose typing - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps({ data }: OptionProps) { - // tslint:disable-line - expect(data.subscribeToMore).toBeTruthy(); - expect(data.subscribeToMore instanceof Function).toBeTruthy(); - done(); - } - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component { + componentWillReceiveProps({ data }: ChildProps) { + // tslint:disable-line + expect(data!.subscribeToMore).toBeTruthy(); + expect(data!.subscribeToMore instanceof Function).toBeTruthy(); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -136,7 +136,7 @@ describe('[queries] api', () => { }); it('exposes fetchMore as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people($skip: Int, $first: Int) { allPeople(first: $first, skip: $skip) { people { @@ -147,9 +147,14 @@ describe('[queries] api', () => { `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; const data1 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; + + type Data = typeof data; + const variables = { skip: 1, first: 1 }; const variables2 = { skip: 2, first: 1 }; + type Variables = typeof variables; + const link = mockSingleLink( { request: { query, variables }, result: { data } }, { request: { query, variables: variables2 }, result: { data: data1 } }, @@ -160,51 +165,54 @@ describe('[queries] api', () => { }); let count = 0; - @graphql(query, { options: () => ({ variables }) }) - class Container extends React.Component { - componentWillMount() { - expect(this.props.data.fetchMore).toBeTruthy(); - expect(this.props.data.fetchMore instanceof Function).toBeTruthy(); - } - componentWillReceiveProps(props: OptionProps) { - if (count === 0) { - expect(props.data.fetchMore).toBeTruthy(); - expect(props.data.fetchMore instanceof Function).toBeTruthy(); - props.data - .fetchMore({ - variables: { skip: 2 }, - updateQuery: (prev, { fetchMoreResult }) => ({ - allPeople: { - people: prev.allPeople.people.concat( - fetchMoreResult.allPeople.people, - ), - }, - }), - }) - .then( - wrap(done, result => { - expect(stripSymbols(result.data.allPeople.people)).toEqual( - data1.allPeople.people, - ); - }), + const Container = graphql<{}, Data, Variables>(query, { + options: () => ({ variables }), + })( + class extends React.Component> { + componentWillMount() { + expect(this.props.data!.fetchMore).toBeTruthy(); + expect(this.props.data!.fetchMore instanceof Function).toBeTruthy(); + } + componentWillReceiveProps(props: ChildProps<{}, Data, Variables>) { + if (count === 0) { + expect(props.data!.fetchMore).toBeTruthy(); + expect(props.data!.fetchMore instanceof Function).toBeTruthy(); + props.data! + .fetchMore({ + variables: { skip: 2 }, + updateQuery: (prev, { fetchMoreResult }) => ({ + allPeople: { + people: prev.allPeople.people.concat( + fetchMoreResult!.allPeople.people, + ), + }, + }), + }) + .then( + wrap(done, result => { + expect(stripSymbols(result.data.allPeople.people)).toEqual( + data1.allPeople.people, + ); + }), + ); + } else if (count === 1) { + expect(stripSymbols(props.data!.variables)).toEqual(variables); + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople!.people)).toEqual( + data.allPeople.people.concat(data1.allPeople.people), ); - } else if (count === 1) { - expect(stripSymbols(props.data.variables)).toEqual(variables); - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople.people)).toEqual( - data.allPeople.people.concat(data1.allPeople.people), - ); + done(); + } else { + throw new Error('should not reach this point'); + } + count++; done(); - } else { - throw new Error('should not reach this point'); } - count++; - done(); - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( @@ -214,7 +222,7 @@ describe('[queries] api', () => { }); it('reruns props function after query results change via fetchMore', done => { - const query = gql` + const query: DocumentNode = gql` query people($cursor: Int) { allPeople(cursor: $cursor) { cursor @@ -232,6 +240,10 @@ describe('[queries] api', () => { const data2 = { allPeople: { cursor: 2, people: [{ name: 'Leia Skywalker' }] }, }; + + type Data = typeof data1; + type Variables = typeof vars1; + const link = mockSingleLink( { request: { query, variables: vars1 }, result: { data: data1 } }, { request: { query, variables: vars2 }, result: { data: data2 } }, @@ -243,26 +255,28 @@ describe('[queries] api', () => { let isUpdated = false; - interface Props {} - interface Data { - allPeople: { - cursor: any; - people: [{ name: string }]; - }; - } - @graphql(query, { - props({ - data: { loading, allPeople, fetchMore }, - }: ChildProps) { + type FinalProps = + | { loading: true } + | { + loading: false; + people: { name: string }[]; + getMorePeople: () => void; + }; + + const Container = graphql<{}, Data, Variables, FinalProps>(query, { + props({ data }) { + const { loading, allPeople, fetchMore } = data!; + if (loading) return { loading }; - const { cursor, people } = allPeople; + const { cursor, people } = allPeople!; return { + loading: false, people, getMorePeople: () => fetchMore({ variables: { cursor }, updateQuery(prev, { fetchMoreResult }) { - const { allPeople: { cursor, people } } = fetchMoreResult; // tslint:disable-line:no-shadowed-variable + const { allPeople: { cursor, people } } = fetchMoreResult!; // tslint:disable-line:no-shadowed-variable return { allPeople: { cursor, @@ -273,25 +287,26 @@ describe('[queries] api', () => { }), }; }, - }) - class Container extends React.Component> { - componentWillReceiveProps(props) { - if (props.loading) return; + })( + class extends React.Component { + componentWillReceiveProps(props: FinalProps) { + if (props.loading) return; - if (isUpdated) { - expect(props.people.length).toBe(2); - done(); - return; - } + if (isUpdated) { + expect(props.people.length).toBe(2); + done(); + return; + } - isUpdated = true; - expect(stripSymbols(props.people)).toEqual(data1.allPeople.people); - props.getMorePeople(); - } - render() { - return null; - } - } + isUpdated = true; + expect(stripSymbols(props.people)).toEqual(data1.allPeople.people); + props.getMorePeople(); + } + render() { + return null; + } + }, + ); renderer.create( diff --git a/test/client/graphql/queries/errors.test.tsx b/test/client/graphql/queries/errors.test.tsx index ef74d02cb4..a53d9e1628 100644 --- a/test/client/graphql/queries/errors.test.tsx +++ b/test/client/graphql/queries/errors.test.tsx @@ -8,9 +8,11 @@ import { mockSingleLink } from '../../../../src/test-utils'; import { ApolloProvider, graphql } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { ChildProps } from '../../../../src/browser'; +import { DocumentNode } from 'graphql'; describe('[queries] errors', () => { - let error; + let error: typeof console.error; beforeEach(() => { error = console.error; console.error = jest.fn(() => {}); // tslint:disable-line @@ -21,7 +23,7 @@ describe('[queries] errors', () => { // errors it('does not swallow children errors', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -41,7 +43,7 @@ describe('[queries] errors', () => { }); class ErrorBoundary extends React.Component { - componentDidCatch(e) { + componentDidCatch(e: Error) { expect(e.message).toMatch(/bar is not a function/); done(); } @@ -50,7 +52,7 @@ describe('[queries] errors', () => { return this.props.children; } } - let bar; + let bar: any; const ContainerWithData = graphql(query)(() => { bar(); // this will throw return null; @@ -66,7 +68,7 @@ describe('[queries] errors', () => { }); it('can unmount without error', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -102,7 +104,7 @@ describe('[queries] errors', () => { }); it('passes any GraphQL errors in props', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -120,19 +122,19 @@ describe('[queries] errors', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class ErrorContainer extends React.Component { - componentWillReceiveProps({ data }) { - // tslint:disable-line - expect(data.error).toBeTruthy(); - expect(data.error.networkError).toBeTruthy(); - // expect(data.error instanceof ApolloError).toBeTruthy(); - done(); - } - render() { - return null; - } - } + const ErrorContainer = graphql(query)( + class extends React.Component { + componentWillReceiveProps({ data }: ChildProps) { + expect(data!.error).toBeTruthy(); + expect(data!.error!.networkError).toBeTruthy(); + // expect(data.error instanceof ApolloError).toBeTruthy(); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -142,8 +144,8 @@ describe('[queries] errors', () => { }); describe('uncaught exceptions', () => { - let unhandled = []; - function handle(reason) { + let unhandled: any[] = []; + function handle(reason: any) { unhandled.push(reason); } beforeEach(() => { @@ -155,7 +157,7 @@ describe('[queries] errors', () => { }); it('does not log when you change variables resulting in an error', done => { - const query = gql` + const query: DocumentNode = gql` query people($var: Int) { allPeople(first: $var) { people { @@ -182,33 +184,45 @@ describe('[queries] errors', () => { cache: new Cache({ addTypename: false }), }); - let iteration = 0; - @withState('var', 'setVar', 1) - @graphql(query) - class ErrorContainer extends React.Component { - componentWillReceiveProps(props) { - // tslint:disable-line - iteration += 1; - if (iteration === 1) { - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - props.setVar(2); - } else if (iteration === 2) { - expect(props.data.loading).toBeTruthy(); - } else if (iteration === 3) { - expect(props.data.error).toBeTruthy(); - expect(props.data.error.networkError).toBeTruthy(); - // We need to set a timeout to ensure the unhandled rejection is swept up - setTimeout(() => { - expect(unhandled.length).toEqual(0); - done(); - }, 0); - } - } - render() { - return null; - } + type Data = typeof data; + type Vars = typeof var1; + + interface Props extends Vars { + var: number; + setVar: (val: number) => number; } + let iteration = 0; + const ErrorContainer = withState('var', 'setVar', 1)( + graphql(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps) { + // tslint:disable-line + iteration += 1; + if (iteration === 1) { + expect(stripSymbols(props.data!.allPeople)).toEqual( + data.allPeople, + ); + props.setVar(2); + } else if (iteration === 2) { + expect(props.data!.loading).toBeTruthy(); + } else if (iteration === 3) { + expect(props.data!.error).toBeTruthy(); + expect(props.data!.error!.networkError).toBeTruthy(); + // We need to set a timeout to ensure the unhandled rejection is swept up + setTimeout(() => { + expect(unhandled.length).toEqual(0); + done(); + }, 0); + } + } + render() { + return null; + } + }, + ), + ); + renderer.create( @@ -219,7 +233,7 @@ describe('[queries] errors', () => { it('will not log a warning when there is an error that is caught in the render method', () => new Promise((resolve, reject) => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -228,6 +242,12 @@ describe('[queries] errors', () => { } } `; + + interface Data { + allPeople: { + people: { name: string }[]; + }; + } const link = mockSingleLink({ request: { query }, error: new Error('oops'), @@ -242,16 +262,18 @@ describe('[queries] errors', () => { console.error = errorMock; let renderCount = 0; - @graphql(query) - class HandledErrorComponent extends React.Component { - render() { + @graphql<{}, Data>(query) + class HandledErrorComponent extends React.Component< + ChildProps<{}, Data> + > { + render(): React.ReactNode { try { switch (renderCount++) { case 0: - expect(this.props.data.loading).toEqual(true); + expect(this.props.data!.loading).toEqual(true); break; case 1: - expect(this.props.data.error.message).toEqual( + expect(this.props.data!.error!.message).toEqual( 'Network error: oops', ); break; @@ -286,7 +308,7 @@ describe('[queries] errors', () => { it('will log a warning when there is an error that is not caught in the render method', () => new Promise((resolve, reject) => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -295,6 +317,13 @@ describe('[queries] errors', () => { } } `; + + interface Data { + allPeople: { + people: { name: string }[]; + }; + } + const link = mockSingleLink({ request: { query }, error: new Error('oops'), @@ -309,13 +338,15 @@ describe('[queries] errors', () => { console.error = errorMock; let renderCount = 0; - @graphql(query) - class UnhandledErrorComponent extends React.Component { - render() { + @graphql<{}, Data>(query) + class UnhandledErrorComponent extends React.Component< + ChildProps<{}, Data> + > { + render(): React.ReactNode { try { switch (renderCount++) { case 0: - expect(this.props.data.loading).toEqual(true); + expect(this.props.data!.loading).toEqual(true); break; case 1: // Noop. Don’t handle the error so a warning will be logged to the console. @@ -354,7 +385,7 @@ describe('[queries] errors', () => { })); it('passes any cached data when there is a GraphQL error', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -364,6 +395,7 @@ describe('[queries] errors', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; const link = mockSingleLink( { request: { query }, result: { data } }, { request: { query }, error: new Error('No Network Connection') }, @@ -374,43 +406,46 @@ describe('[queries] errors', () => { }); let count = 0; - @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Container extends React.Component { - componentWillReceiveProps(props) { - try { - switch (count++) { - case 0: - expect(stripSymbols(props.data.allPeople)).toEqual( - data.allPeople, - ); - props.data.refetch().catch(() => null); - break; - case 1: - expect(props.data.loading).toBeTruthy(); - expect(stripSymbols(props.data.allPeople)).toEqual( - data.allPeople, - ); - break; - case 2: - expect(props.data.loading).toBeFalsy(); - expect(props.data.error).toBeTruthy(); - expect(stripSymbols(props.data.allPeople)).toEqual( - data.allPeople, - ); - done(); - break; - default: - throw new Error('Unexpected fall through'); + const Container = graphql<{}, Data>(query, { + options: { notifyOnNetworkStatusChange: true }, + })( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + try { + switch (count++) { + case 0: + expect(stripSymbols(props.data!.allPeople)).toEqual( + data.allPeople, + ); + props.data!.refetch().catch(() => null); + break; + case 1: + expect(props.data!.loading).toBeTruthy(); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data.allPeople, + ); + break; + case 2: + expect(props.data!.loading).toBeFalsy(); + expect(props.data!.error).toBeTruthy(); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data.allPeople, + ); + done(); + break; + default: + throw new Error('Unexpected fall through'); + } + } catch (e) { + done.fail(e); } - } catch (e) { - done.fail(e); } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( @@ -420,7 +455,7 @@ describe('[queries] errors', () => { }); it('can refetch after there was a network error', done => { - const query = gql` + const query: DocumentNode = gql` query somethingelse { allPeople(first: 1) { people { @@ -431,6 +466,8 @@ describe('[queries] errors', () => { `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; const dataTwo = { allPeople: { people: [{ name: 'Princess Leia' }] } }; + + type Data = typeof data; const link = mockSingleLink( { request: { query }, result: { data } }, { request: { query }, error: new Error('This is an error!') }, @@ -443,57 +480,60 @@ describe('[queries] errors', () => { let count = 0; const noop = () => null; - @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Container extends React.Component { - componentWillReceiveProps(props) { - try { - switch (count++) { - case 0: - props.data - .refetch() - .then(() => { - done.fail('Expected error value on first refetch.'); - }) - .catch(noop); - break; - case 1: - expect(props.data.loading).toBeTruthy(); - break; - case 2: - expect(props.data.loading).toBeFalsy(); - expect(props.data.error).toBeTruthy(); - props.data - .refetch() - .then(noop) - .catch(() => { - done.fail('Expected good data on second refetch.'); - }); - break; - // Further fix required in QueryManager - // case 3: - // expect(props.data.loading).toBeTruthy(); - // expect(props.data.error).toBeFalsy(); - // break; - case 3: - expect(props.data.loading).toBeFalsy(); - expect(props.data.error).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual( - dataTwo.allPeople, - ); - done(); - break; - default: - throw new Error('Unexpected fall through'); + const Container = graphql<{}, Data>(query, { + options: { notifyOnNetworkStatusChange: true }, + })( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + try { + switch (count++) { + case 0: + props.data! + .refetch() + .then(() => { + done.fail('Expected error value on first refetch.'); + }) + .catch(noop); + break; + case 1: + expect(props.data!.loading).toBeTruthy(); + break; + case 2: + expect(props.data!.loading).toBeFalsy(); + expect(props.data!.error).toBeTruthy(); + props.data! + .refetch() + .then(noop) + .catch(() => { + done.fail('Expected good data on second refetch.'); + }); + break; + // Further fix required in QueryManager + // case 3: + // expect(props.data.loading).toBeTruthy(); + // expect(props.data.error).toBeFalsy(); + // break; + case 3: + expect(props.data!.loading).toBeFalsy(); + expect(props.data!.error).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual( + dataTwo.allPeople, + ); + done(); + break; + default: + throw new Error('Unexpected fall through'); + } + } catch (e) { + done.fail(e); } - } catch (e) { - done.fail(e); } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( diff --git a/test/client/graphql/queries/index.test.tsx b/test/client/graphql/queries/index.test.tsx index 0909141d6e..c65633f59b 100644 --- a/test/client/graphql/queries/index.test.tsx +++ b/test/client/graphql/queries/index.test.tsx @@ -5,13 +5,19 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql, DataProps } from '../../../../src'; +import { + ApolloProvider, + graphql, + DataProps, + ChildProps, +} from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; import catchAsyncError from '../../../test-utils/catchAsyncError'; +import { DocumentNode } from 'graphql'; describe('queries', () => { - let error; + let error: typeof console.error; beforeEach(() => { error = console.error; console.error = jest.fn(() => {}); // tslint:disable-line @@ -22,7 +28,7 @@ describe('queries', () => { // general api it('binds a query to props', () => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -63,7 +69,7 @@ describe('queries', () => { }); it('includes the variables in the props', () => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -97,10 +103,10 @@ describe('queries', () => { first: number; } - const ContainerWithData = graphql(query)( - ({ data }: DataProps) => { + const ContainerWithData = graphql(query)( + ({ data }: ChildProps) => { expect(data).toBeTruthy(); - expect(data.variables).toEqual(variables); + expect(data!.variables).toEqual(variables); return null; }, ); @@ -114,8 +120,8 @@ describe('queries', () => { it("shouldn't warn about fragments", () => { const oldWarn = console.warn; - const warnings = []; - console.warn = str => warnings.push(str); + const warnings: any[] = []; + console.warn = (str: any) => warnings.push(str); try { graphql( @@ -132,7 +138,7 @@ describe('queries', () => { }); it('executes a query', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -142,6 +148,8 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -151,17 +159,18 @@ describe('queries', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - done(); - } - render() { - return null; - } - } + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -171,7 +180,7 @@ describe('queries', () => { }); it('executes a query with two root fields', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -189,6 +198,8 @@ describe('queries', () => { allPeople: { people: [{ name: 'Luke Skywalker' }] }, otherPeople: { people: [{ name: 'Luke Skywalker' }] }, }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -198,18 +209,21 @@ describe('queries', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - expect(stripSymbols(props.data.otherPeople)).toEqual(data.otherPeople); - done(); - } - render() { - return null; - } - } + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + expect(stripSymbols(props.data!.otherPeople)).toEqual( + data.otherPeople, + ); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -219,7 +233,7 @@ describe('queries', () => { }); it('maps props as variables if they match', done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -229,7 +243,11 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const variables = { first: 1 }; + type Vars = typeof variables; + const link = mockSingleLink({ request: { query, variables }, result: { data }, @@ -239,20 +257,21 @@ describe('queries', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - expect(stripSymbols(props.data.variables)).toEqual( - this.props.data.variables, - ); - done(); - } - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + expect(stripSymbols(props.data!.variables)).toEqual( + this.props.data!.variables, + ); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -262,7 +281,7 @@ describe('queries', () => { }); it("doesn't care about the order of variables in a request", done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int, $jedi: Boolean) { allPeople(first: $first, jedi: $jedi) { people { @@ -272,7 +291,10 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; const variables = { jedi: true, first: 1 }; + type Vars = typeof variables; + const mocks = [ { request: { @@ -298,19 +320,20 @@ describe('queries', () => { }, }; - @graphql(query, options) - class Container extends React.Component { - componentWillReceiveProps(props) { - catchAsyncError(done, () => { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - done(); - }); - } - render() { - return null; - } - } + const Container = graphql<{}, Data, Vars>(query, options)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data, Vars>) { + catchAsyncError(done, () => { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + done(); + }); + } + render() { + return null; + } + }, + ); renderer.create( @@ -320,7 +343,7 @@ describe('queries', () => { }); it('allows falsy values in the mapped variables from props', done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -330,7 +353,11 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const variables = { first: null }; + type Vars = typeof variables; + const link = mockSingleLink({ request: { query, variables }, result: { data }, @@ -340,17 +367,20 @@ describe('queries', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - done(); - } - render() { - return null; - } - } + const Container = graphql, Data, Vars>(query)( + class extends React.Component, Data, Vars>> { + componentWillReceiveProps( + props: ChildProps, Data, Vars>, + ) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -360,7 +390,7 @@ describe('queries', () => { }); it("doesn't error on optional required props", () => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -370,7 +400,11 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const variables = { first: 1 }; + type Vars = typeof variables; + const link = mockSingleLink({ request: { query, variables }, result: { data }, @@ -379,7 +413,7 @@ describe('queries', () => { link, cache: new Cache({ addTypename: false }), }); - const Container = graphql(query)(() => null); + const Container = graphql(query)(() => null); let errorCaught = null; try { @@ -397,7 +431,7 @@ describe('queries', () => { // note this should log an error in the console until they are all cleaned up with react 16 it("errors if the passed props don't contain the needed variables", done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int!) { allPeople(first: $first) { people { @@ -407,7 +441,11 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const variables = { first: 1 }; + type Vars = typeof variables; + const link = mockSingleLink({ request: { query, variables }, result: { data }, @@ -416,9 +454,13 @@ describe('queries', () => { link, cache: new Cache({ addTypename: false }), }); - const Container = graphql(query)(() => null); + + interface WrongProps { + frst: number; + } + const Container = graphql(query)(() => null); class ErrorBoundary extends React.Component { - componentDidCatch(e) { + componentDidCatch(e: Error) { expect(e.name).toMatch(/Invariant Violation/); expect(e.message).toMatch(/The operation 'people'/); done(); @@ -439,7 +481,7 @@ describe('queries', () => { // context it('allows context through updates', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -449,6 +491,8 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -458,19 +502,20 @@ describe('queries', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(props.data.loading).toBeFalsy(); - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - } - render() { - return
{this.props.children}
; - } - } + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + expect(props.data!.loading).toBeFalsy(); + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + } + render() { + return
{this.props.children}
; + } + }, + ); - class ContextContainer extends React.Component { - constructor(props) { + class ContextContainer extends React.Component<{}, { color: string }> { + constructor(props: {}) { super(props); this.state = { color: 'purple' }; } @@ -526,7 +571,7 @@ describe('queries', () => { // meta it('stores the component name in the query metadata', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -536,6 +581,8 @@ describe('queries', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -545,24 +592,26 @@ describe('queries', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps() { - const queries = client.queryManager.queryStore.getStore(); - const queryIds = Object.keys(queries); - expect(queryIds.length).toEqual(1); - const queryFirst = queries[queryIds[0]]; - expect(queryFirst.metadata).toEqual({ - reactComponent: { - displayName: 'Apollo(Container)', - }, - }); - done(); - } - render() { - return null; - } - } + const Container = graphql<{}, Data>(query)( + // tslint:disable-next-line:no-shadowed-variable + class Container extends React.Component> { + componentWillReceiveProps() { + const queries = client.queryManager.queryStore.getStore(); + const queryIds = Object.keys(queries); + expect(queryIds.length).toEqual(1); + const queryFirst = queries[queryIds[0]]; + expect(queryFirst.metadata).toEqual({ + reactComponent: { + displayName: 'Apollo(Container)', + }, + }); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -572,7 +621,7 @@ describe('queries', () => { }); it("uses a custom wrapped component name when 'alias' is specified", () => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -585,7 +634,7 @@ describe('queries', () => { alias: 'withFoo', }) class Container extends React.Component { - render() { + render(): React.ReactNode { return null; } } diff --git a/test/client/graphql/queries/lifecycle.test.tsx b/test/client/graphql/queries/lifecycle.test.tsx index 2d3cff5a74..d98377c418 100644 --- a/test/client/graphql/queries/lifecycle.test.tsx +++ b/test/client/graphql/queries/lifecycle.test.tsx @@ -1,20 +1,21 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import wait from '../../../test-utils/wait'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('[queries] lifecycle', () => { // lifecycle it('reruns the query if it changes', done => { let count = 0; - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -25,7 +26,9 @@ describe('[queries] lifecycle', () => { `; const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; const variables1 = { first: 1 }; + type Vars = typeof variables1; const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const variables2 = { first: 2 }; @@ -40,29 +43,30 @@ describe('[queries] lifecycle', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { + const Container = graphql(query, { options: props => ({ variables: props, fetchPolicy: count === 0 ? 'cache-and-network' : 'cache-first', }), - }) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // loading is true, but data still there - if (count === 1 && data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data1.allPeople); + })( + class extends React.Component> { + componentWillReceiveProps({ data }: ChildProps) { + // loading is true, but data still there + if (count === 1 && data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data1.allPeople); + } + if (count === 1 && !data!.loading && this.props.data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data2.allPeople); + done(); + } } - if (count === 1 && !data.loading && this.props.data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data2.allPeople); - done(); + render() { + return null; } - } - render() { - return null; - } - } + }, + ); - class ChangingProps extends React.Component { + class ChangingProps extends React.Component<{}, { first: number }> { state = { first: 1 }; componentDidMount() { @@ -85,7 +89,7 @@ describe('[queries] lifecycle', () => { }); it('rebuilds the queries on prop change when using `options`', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -95,6 +99,8 @@ describe('[queries] lifecycle', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -106,7 +112,7 @@ describe('[queries] lifecycle', () => { let firstRun = true; let isDone = false; - function options(props) { + function options(props: Props) { if (!firstRun) { expect(props.listId).toBe(2); if (!isDone) done(); @@ -114,10 +120,13 @@ describe('[queries] lifecycle', () => { } return {}; } + interface Props { + listId: number; + } - const Container = graphql(query, { options })(props => null); + const Container = graphql(query, { options })(() => null); - class ChangingProps extends React.Component { + class ChangingProps extends React.Component<{}, { listId: number }> { state = { listId: 1 }; componentDidMount() { @@ -141,7 +150,7 @@ describe('[queries] lifecycle', () => { it('reruns the query if just the variables change', done => { let count = 0; - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -152,7 +161,10 @@ describe('[queries] lifecycle', () => { `; const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; + const variables1 = { first: 1 }; + type Vars = typeof variables1; const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const variables2 = { first: 2 }; @@ -167,24 +179,27 @@ describe('[queries] lifecycle', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { options: props => ({ variables: props }) }) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // loading is true, but data still there - if (count === 1 && data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data1.allPeople); + const Container = graphql(query, { + options: props => ({ variables: props }), + })( + class extends React.Component> { + componentWillReceiveProps({ data }: ChildProps) { + // loading is true, but data still there + if (count === 1 && data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data1.allPeople); + } + if (count === 1 && !data!.loading && this.props.data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data2.allPeople); + done(); + } } - if (count === 1 && !data.loading && this.props.data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data2.allPeople); - done(); + render() { + return null; } - } - render() { - return null; - } - } + }, + ); - class ChangingProps extends React.Component { + class ChangingProps extends React.Component { state = { first: 1 }; componentDidMount() { @@ -208,7 +223,7 @@ describe('[queries] lifecycle', () => { it('reruns the queries on prop change when using passed props', done => { let count = 0; - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -219,7 +234,10 @@ describe('[queries] lifecycle', () => { `; const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; + const variables1 = { first: 1 }; + type Vars = typeof variables1; const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const variables2 = { first: 2 }; @@ -234,24 +252,25 @@ describe('[queries] lifecycle', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // loading is true, but data still there - if (count === 1 && data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data1.allPeople); + const Container = graphql(query)( + class extends React.Component> { + componentWillReceiveProps({ data }: ChildProps) { + // loading is true, but data still there + if (count === 1 && data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data1.allPeople); + } + if (count === 1 && !data!.loading && this.props.data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data2.allPeople); + done(); + } } - if (count === 1 && !data.loading && this.props.data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data2.allPeople); - done(); + render() { + return null; } - } - render() { - return null; - } - } + }, + ); - class ChangingProps extends React.Component { + class ChangingProps extends React.Component { state = { first: 1 }; componentDidMount() { @@ -274,7 +293,7 @@ describe('[queries] lifecycle', () => { }); it('stays subscribed to updates after irrelevant prop changes', done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -284,7 +303,10 @@ describe('[queries] lifecycle', () => { } `; const variables = { first: 1 }; + type Vars = typeof variables; const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; + const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const link = mockSingleLink( { request: { query, variables }, result: { data: data1 } }, @@ -295,39 +317,50 @@ describe('[queries] lifecycle', () => { cache: new Cache({ addTypename: false }), }); + interface Props { + foo: number; + changeState: () => void; + } + let count = 0; - @graphql(query, { + const Container = graphql(query, { options: { variables, notifyOnNetworkStatusChange: false }, - }) - class Container extends React.Component { - 8; - componentWillReceiveProps(props) { - count += 1; - try { - if (count === 1) { - expect(props.foo).toEqual(42); - expect(props.data.loading).toEqual(false); - expect(stripSymbols(props.data.allPeople)).toEqual(data1.allPeople); - props.changeState(); - } else if (count === 2) { - expect(props.foo).toEqual(43); - expect(props.data.loading).toEqual(false); - expect(stripSymbols(props.data.allPeople)).toEqual(data1.allPeople); - props.data.refetch(); - } else if (count === 3) { - expect(props.foo).toEqual(43); - expect(props.data.loading).toEqual(false); - expect(stripSymbols(props.data.allPeople)).toEqual(data2.allPeople); - done(); + })( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps) { + count += 1; + try { + if (count === 1) { + expect(props.foo).toEqual(42); + expect(props.data!.loading).toEqual(false); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data1.allPeople, + ); + props.changeState(); + } else if (count === 2) { + expect(props.foo).toEqual(43); + expect(props.data!.loading).toEqual(false); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data1.allPeople, + ); + props.data!.refetch(); + } else if (count === 3) { + expect(props.foo).toEqual(43); + expect(props.data!.loading).toEqual(false); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data2.allPeople, + ); + done(); + } + } catch (e) { + done.fail(e); } - } catch (e) { - done.fail(e); } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); class Parent extends React.Component { state = { foo: 42 }; @@ -349,7 +382,7 @@ describe('[queries] lifecycle', () => { }); it('correctly rebuilds props on remount', done => { - const query = gql` + const query: DocumentNode = gql` query pollingPeople { allPeople(first: 1) { people { @@ -359,6 +392,7 @@ describe('[queries] lifecycle', () => { } `; const data = { allPeople: { people: [{ name: 'Darth Skywalker' }] } }; + type Data = typeof data; const link = mockSingleLink({ request: { query }, result: { data }, @@ -374,31 +408,32 @@ describe('[queries] lifecycle', () => { link, cache: new Cache({ addTypename: false }), }); - let wrapper, - app, + let wrapper: ReactWrapper, + app: React.ReactElement, count = 0; - @graphql(query, { + const Container = graphql<{}, Data>(query, { options: { pollInterval: 10, notifyOnNetworkStatusChange: false }, - }) - class Container extends React.Component { - componentWillReceiveProps(props) { - if (count === 1) { - // has data - wrapper.unmount(); - wrapper = mount(app); - } + })( + class extends React.Component> { + componentWillReceiveProps() { + if (count === 1) { + // has data + wrapper.unmount(); + wrapper = mount(app); + } - if (count === 10) { - wrapper.unmount(); - done(); + if (count === 10) { + wrapper.unmount(); + done(); + } + count++; } - count++; - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); app = ( @@ -410,7 +445,7 @@ describe('[queries] lifecycle', () => { }); it('will re-execute a query when the client changes', async () => { - const query = gql` + const query: DocumentNode = gql` { a b @@ -471,22 +506,31 @@ describe('[queries] lifecycle', () => { link: link3, cache: new Cache({ addTypename: false }), }); - const renders = []; - let switchClient; - let refetchQuery; - @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Query extends React.Component { - componentDidMount() { - refetchQuery = () => this.props.data.refetch(); - } - - render() { - const { data: { loading, a, b, c } } = this.props; - renders.push({ loading, a, b, c }); - return null; - } + interface Data { + a: number; + b: number; + c: number; } + const renders: (Partial & { loading: boolean })[] = []; + let switchClient: (client: ApolloClient) => void; + let refetchQuery: () => void; + + const Query = graphql<{}, Data>(query, { + options: { notifyOnNetworkStatusChange: true }, + })( + class extends React.Component> { + componentDidMount() { + refetchQuery = () => this.props.data!.refetch(); + } + + render() { + const { loading, a, b, c } = this.props.data!; + renders.push({ loading, a, b, c }); + return null; + } + }, + ); class ClientSwitcher extends React.Component { state = { @@ -511,19 +555,19 @@ describe('[queries] lifecycle', () => { renderer.create(); await wait(1); - refetchQuery(); + refetchQuery!(); await wait(1); - switchClient(client2); + switchClient!(client2); await wait(1); - refetchQuery(); + refetchQuery!(); await wait(1); - switchClient(client3); + switchClient!(client3); await wait(1); - switchClient(client1); + switchClient!(client1); await wait(1); - switchClient(client2); + switchClient!(client2); await wait(1); - switchClient(client3); + switchClient!(client3); await wait(1); expect(renders).toEqual([ @@ -544,7 +588,7 @@ describe('[queries] lifecycle', () => { }); it('handles racecondition with prefilled data from the server', async done => { - const query = gql` + const query: DocumentNode = gql` query GetUser($first: Int) { user(first: $first) { name @@ -552,7 +596,9 @@ describe('[queries] lifecycle', () => { } `; const variables = { first: 1 }; + type Vars = typeof variables; const data2 = { user: { name: 'Luke Skywalker' } }; + type Data = typeof data2; const link = mockSingleLink({ request: { query, variables }, @@ -577,28 +623,29 @@ describe('[queries] lifecycle', () => { }); let count = 0; - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - count++; - } + const Container = graphql(query)( + class extends React.Component> { + componentWillReceiveProps() { + count++; + } - componentDidMount() { - this.props.data.refetch().then(result => { - expect(result.data.user.name).toBe('Luke Skywalker'); - done(); - }); - } + componentDidMount() { + this.props.data!.refetch().then(result => { + expect(result.data.user.name).toBe('Luke Skywalker'); + done(); + }); + } - render() { - const user = this.props.data.user || {}; - const { name = '' } = user; - if (count === 2) { - expect(name).toBe('Luke Skywalker'); + render() { + const user = this.props.data!.user; + const name = user ? user.name : ''; + if (count === 2) { + expect(name).toBe('Luke Skywalker'); + } + return null; } - return null; - } - } + }, + ); mount( diff --git a/test/client/graphql/queries/loading.test.tsx b/test/client/graphql/queries/loading.test.tsx index d04f156c52..32ea63531e 100644 --- a/test/client/graphql/queries/loading.test.tsx +++ b/test/client/graphql/queries/loading.test.tsx @@ -3,17 +3,18 @@ import * as renderer from 'react-test-renderer'; import * as ReactDOM from 'react-dom'; import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, DataProps, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('[queries] loading', () => { // networkStatus / loading it('exposes networkStatus as a part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -31,16 +32,19 @@ describe('[queries] loading', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Container extends React.Component { - componentWillReceiveProps({ data }: DataProps) { - expect(data.networkStatus).toBeTruthy(); - done(); - } - render() { - return null; - } - } + const Container = graphql(query, { + options: { notifyOnNetworkStatusChange: true }, + })( + class extends React.Component { + componentWillReceiveProps({ data }: ChildProps) { + expect(data!.networkStatus).toBeTruthy(); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -50,7 +54,7 @@ describe('[queries] loading', () => { }); it('should set the initial networkStatus to 1 (loading)', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -70,15 +74,15 @@ describe('[queries] loading', () => { }); @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Container extends React.Component { - constructor(props: DataProps) { + class Container extends React.Component { + constructor(props: ChildProps) { super(props); - const { data: { networkStatus } } = props; + const { networkStatus } = props.data!; expect(networkStatus).toBe(1); done(); } - render() { + render(): React.ReactNode { return null; } } @@ -91,7 +95,7 @@ describe('[queries] loading', () => { }); it('should set the networkStatus to 7 (ready) when the query is loaded', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -110,17 +114,20 @@ describe('[queries] loading', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Container extends React.Component { - componentWillReceiveProps({ data: { networkStatus } }) { - expect(networkStatus).toBe(7); - done(); - } + const Container = graphql(query, { + options: { notifyOnNetworkStatusChange: true }, + })( + class extends React.Component { + componentWillReceiveProps(nextProps: ChildProps) { + expect(nextProps.data!.networkStatus).toBe(7); + done(); + } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( @@ -131,7 +138,7 @@ describe('[queries] loading', () => { it('should set the networkStatus to 2 (setVariables) when the query variables are changed', done => { let count = 0; - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -147,6 +154,9 @@ describe('[queries] loading', () => { const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const variables2 = { first: 2 }; + type Data = typeof data1; + type Vars = typeof variables1; + const link = mockSingleLink( { request: { query, variables: variables1 }, result: { data: data1 } }, { request: { query, variables: variables2 }, result: { data: data2 } }, @@ -157,34 +167,39 @@ describe('[queries] loading', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { + const Container = graphql(query, { options: props => ({ variables: props, notifyOnNetworkStatusChange: true, }), - }) - class Container extends React.Component { - componentWillReceiveProps(nextProps) { - // variables changed, new query is loading, but old data is still there - if (count === 1 && nextProps.data.loading) { - expect(nextProps.data.networkStatus).toBe(2); - expect(stripSymbols(nextProps.data.allPeople)).toEqual( - data1.allPeople, - ); + })( + class extends React.Component> { + componentWillReceiveProps(nextProps: ChildProps) { + // variables changed, new query is loading, but old data is still there + if (count === 1 && nextProps.data!.loading) { + expect(nextProps.data!.networkStatus).toBe(2); + expect(stripSymbols(nextProps.data!.allPeople)).toEqual( + data1.allPeople, + ); + } + // query with new variables is loaded + if ( + count === 1 && + !nextProps.data!.loading && + this.props.data!.loading + ) { + expect(nextProps.data!.networkStatus).toBe(7); + expect(stripSymbols(nextProps.data!.allPeople)).toEqual( + data2.allPeople, + ); + done(); + } } - // query with new variables is loaded - if (count === 1 && !nextProps.data.loading && this.props.data.loading) { - expect(nextProps.data.networkStatus).toBe(7); - expect(stripSymbols(nextProps.data.allPeople)).toEqual( - data2.allPeople, - ); - done(); + render() { + return null; } - } - render() { - return null; - } - } + }, + ); class ChangingProps extends React.Component { state = { first: 1 }; @@ -210,7 +225,7 @@ describe('[queries] loading', () => { it('resets the loading state after a refetched query', () => new Promise((resolve, reject) => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -221,6 +236,9 @@ describe('[queries] loading', () => { `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; + + type Data = typeof data; + const link = mockSingleLink( { request: { query }, result: { data } }, { request: { query }, result: { data: data2 } }, @@ -231,39 +249,42 @@ describe('[queries] loading', () => { }); let count = 0; - @graphql(query, { options: { notifyOnNetworkStatusChange: true } }) - class Container extends React.Component { - componentWillReceiveProps(props) { - switch (count++) { - case 0: - expect(props.data.networkStatus).toBe(7); - // this isn't reloading fully - props.data.refetch(); - break; - case 1: - expect(props.data.loading).toBeTruthy(); - expect(props.data.networkStatus).toBe(4); - expect(stripSymbols(props.data.allPeople)).toEqual( - data.allPeople, - ); - break; - case 2: - expect(props.data.loading).toBeFalsy(); - expect(props.data.networkStatus).toBe(7); - expect(stripSymbols(props.data.allPeople)).toEqual( - data2.allPeople, - ); - resolve(); - break; - default: - reject(new Error('Too many props updates')); + const Container = graphql<{}, Data>(query, { + options: { notifyOnNetworkStatusChange: true }, + })( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + switch (count++) { + case 0: + expect(props.data!.networkStatus).toBe(7); + // this isn't reloading fully + props.data!.refetch(); + break; + case 1: + expect(props.data!.loading).toBeTruthy(); + expect(props.data!.networkStatus).toBe(4); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data.allPeople, + ); + break; + case 2: + expect(props.data!.loading).toBeFalsy(); + expect(props.data!.networkStatus).toBe(7); + expect(stripSymbols(props.data!.allPeople)).toEqual( + data2.allPeople, + ); + resolve(); + break; + default: + reject(new Error('Too many props updates')); + } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( @@ -273,7 +294,7 @@ describe('[queries] loading', () => { })); it('correctly sets loading state on remounted network-only query', done => { - const query = gql` + const query: DocumentNode = gql` query pollingPeople { allPeople(first: 1) { people { @@ -283,6 +304,8 @@ describe('[queries] loading', () => { } `; const data = { allPeople: { people: [{ name: 'Darth Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -299,44 +322,47 @@ describe('[queries] loading', () => { link, cache: new Cache({ addTypename: false }), }); - let wrapper, - app, + let wrapper: ReactWrapper, + app: React.ReactElement, count = 0; - @graphql(query, { options: { fetchPolicy: 'network-only' } }) - class Container extends React.Component { - componentWillMount() { - if (count === 1) { - expect(this.props.data.loading).toBeTruthy(); // on remount - count++; - } - } - componentWillReceiveProps(props) { - try { - if (count === 0) { - // has data - wrapper.unmount(); - setTimeout(() => { - wrapper = mount(app); - }, 5); + const Container = graphql<{}, Data>(query, { + options: { fetchPolicy: 'network-only' }, + })( + class extends React.Component> { + componentWillMount() { + if (count === 1) { + expect(this.props.data!.loading).toBeTruthy(); // on remount + count++; } - if (count === 3) { - // remounted data after fetch - expect(props.data.loading).toBeFalsy(); - expect(props.data.allPeople.people[0].name).toMatch( - /Darth Skywalker - /, - ); - done(); + } + componentWillReceiveProps(props: ChildProps<{}, Data>) { + try { + if (count === 0) { + // has data + wrapper.unmount(); + setTimeout(() => { + wrapper = mount(app); + }, 5); + } + if (count === 3) { + // remounted data after fetch + expect(props.data!.loading).toBeFalsy(); + expect(props.data!.allPeople!.people[0].name).toMatch( + /Darth Skywalker - /, + ); + done(); + } + } catch (e) { + done.fail(e); } - } catch (e) { - done.fail(e); + count++; } - count++; - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); app = ( @@ -348,7 +374,7 @@ describe('[queries] loading', () => { }); it('correctly sets loading state on remounted component with changed variables', done => { - const query = gql` + const query: DocumentNode = gql` query remount($first: Int) { allPeople(first: $first) { people { @@ -357,9 +383,18 @@ describe('[queries] loading', () => { } } `; + + interface Data { + allPeople: { + people: { name: string }[]; + }; + } const data = { allPeople: null }; const variables = { first: 1 }; const variables2 = { first: 2 }; + + type Vars = typeof variables; + const link = mockSingleLink( { request: { query, variables }, result: { data }, delay: 10 }, { @@ -372,45 +407,46 @@ describe('[queries] loading', () => { link, cache: new Cache({ addTypename: false }), }); - let wrapper, - render, + let wrapper: ReactWrapper, + render: (num: number) => React.ReactElement, count = 0; interface Props { first: number; } - @graphql(query, { + const Container = graphql(query, { options: ({ first }) => ({ variables: { first } }), - }) - class Container extends React.Component { - componentWillMount() { - if (count === 1) { - expect(this.props.data.loading).toBeTruthy(); // on remount - count++; - } - } - componentWillReceiveProps(props) { - if (count === 0) { - // has data - wrapper.unmount(); - setTimeout(() => { - wrapper = mount(render(2)); - }, 5); + })( + class extends React.Component> { + componentWillMount() { + if (count === 1) { + expect(this.props.data!.loading).toBeTruthy(); // on remount + count++; + } } + componentWillReceiveProps(props: ChildProps) { + if (count === 0) { + // has data + wrapper.unmount(); + setTimeout(() => { + wrapper = mount(render(2)); + }, 5); + } - if (count === 2) { - // remounted data after fetch - expect(props.data.loading).toBeFalsy(); - done(); + if (count === 2) { + // remounted data after fetch + expect(props.data!.loading).toBeFalsy(); + done(); + } + count++; } - count++; - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); - render = first => ( + render = (first: number) => ( @@ -420,7 +456,7 @@ describe('[queries] loading', () => { }); it('correctly sets loading state on remounted component with changed variables (alt)', done => { - const query = gql` + const query: DocumentNode = gql` query remount($name: String) { allPeople(name: $name) { people { @@ -429,9 +465,18 @@ describe('[queries] loading', () => { } } `; + + interface Data { + allPeople: { + people: { name: string }[]; + }; + } const data = { allPeople: null }; const variables = { name: 'does-not-exist' }; const variables2 = { name: 'nothing-either' }; + + type Vars = typeof variables; + const link = mockSingleLink( { request: { query, variables }, result: { data }, delay: 10 }, { @@ -446,26 +491,27 @@ describe('[queries] loading', () => { }); let count = 0; - @graphql(query) - class Container extends React.Component { - render() { - const { loading } = this.props.data; - if (count === 0) expect(loading).toBeTruthy(); - if (count === 1) expect(loading).toBeFalsy(); - if (count === 2) expect(loading).toBeTruthy(); - if (count === 3) { - expect(loading).toBeFalsy(); - done(); + const Container = graphql(query)( + class extends React.Component> { + render() { + const { loading } = this.props.data!; + if (count === 0) expect(loading).toBeTruthy(); + if (count === 1) expect(loading).toBeFalsy(); + if (count === 2) expect(loading).toBeTruthy(); + if (count === 3) { + expect(loading).toBeFalsy(); + done(); + } + count++; + return null; } - count++; - return null; - } - } + }, + ); const main = document.createElement('DIV'); main.id = 'main'; document.body.appendChild(main); - const render = props => { + const render = (props: Vars) => { ReactDOM.render( @@ -482,7 +528,7 @@ describe('[queries] loading', () => { }); it('correctly sets loading state on component with changed variables and unchanged result', done => { - const query = gql` + const query: DocumentNode = gql` query remount($first: Int) { allPeople(first: $first) { people { @@ -491,9 +537,17 @@ describe('[queries] loading', () => { } } `; + interface Data { + allPeople: { + people: { name: string }[]; + }; + } + const data = { allPeople: null }; const variables = { first: 1 }; const variables2 = { first: 2 }; + + type Vars = typeof variables; const link = mockSingleLink( { request: { query, variables }, result: { data }, delay: 10 }, { @@ -508,9 +562,15 @@ describe('[queries] loading', () => { }); let count = 0; - const connect = (component): any => { - return class extends React.Component { - constructor(props) { + interface Props extends Vars { + setFirst: (first: number) => void; + } + + const connect = ( + component: React.ComponentType, + ): React.ComponentType<{}> => { + return class extends React.Component<{}, { first: number }> { + constructor(props: {}) { super(props); this.state = { @@ -519,7 +579,7 @@ describe('[queries] loading', () => { this.setFirst = this.setFirst.bind(this); } - setFirst(first) { + setFirst(first: number) { this.setState({ first }); } @@ -532,34 +592,37 @@ describe('[queries] loading', () => { }; }; - @connect - @graphql(query, { - options: ({ first }) => ({ variables: { first } }), - }) - class Container extends React.Component { - componentWillReceiveProps(props) { - if (count === 0) { - expect(props.data.loading).toBeFalsy(); // has initial data - setTimeout(() => { - this.props.setFirst(2); - }, 5); - } + const Container = connect( + graphql(query, { + options: ({ first }) => ({ variables: { first } }), + })( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps) { + if (count === 0) { + expect(props.data!.loading).toBeFalsy(); // has initial data + setTimeout(() => { + this.props.setFirst(2); + }, 5); + } - if (count === 1) { - expect(props.data.loading).toBeTruthy(); // on variables change - } + if (count === 1) { + expect(props.data!.loading).toBeTruthy(); // on variables change + } + + if (count === 2) { + // new data after fetch + expect(props.data!.loading).toBeFalsy(); + done(); + } + count++; + } + render() { + return null; + } + }, + ), + ); - if (count === 2) { - // new data after fetch - expect(props.data.loading).toBeFalsy(); - done(); - } - count++; - } - render() { - return null; - } - } renderer.create( diff --git a/test/client/graphql/queries/observableQuery.test.tsx b/test/client/graphql/queries/observableQuery.test.tsx index f5412c0b2e..f49a0fbf44 100644 --- a/test/client/graphql/queries/observableQuery.test.tsx +++ b/test/client/graphql/queries/observableQuery.test.tsx @@ -1,18 +1,19 @@ import * as React from 'react'; import * as renderer from 'react-test-renderer'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('[queries] observableQuery', () => { // observableQuery it('will recycle `ObservableQuery`s when re-rendering the entire tree', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -22,6 +23,8 @@ describe('[queries] observableQuery', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink( { request: { query }, result: { data } }, { request: { query }, result: { data } }, @@ -35,7 +38,7 @@ describe('[queries] observableQuery', () => { // let queryObservable1: ObservableQuery; // let queryObservable2: ObservableQuery; // let originalOptions; - let wrapper1; + let wrapper1: ReactWrapper; // let wrapper2; let count = 0; // let recycledOptions; @@ -52,56 +55,59 @@ describe('[queries] observableQuery', () => { expect(keys).toEqual(['1']); }; - @graphql(query, { options: { fetchPolicy: 'cache-and-network' } }) - class Container extends React.Component { - componentWillMount() { - // during the first mount, the loading prop should be true; - if (count === 0) { - expect(this.props.data.loading).toBeTruthy(); - } + const Container = graphql<{}, Data>(query, { + options: { fetchPolicy: 'cache-and-network' }, + })( + class extends React.Component> { + componentWillMount() { + // during the first mount, the loading prop should be true; + if (count === 0) { + expect(this.props.data!.loading).toBeTruthy(); + } - // during the second mount, the loading prop should be false, and data should - // be present; - if (count === 3) { - expect(this.props.data.loading).toBeFalsy(); - expect(stripSymbols(this.props.data.allPeople)).toEqual( - data.allPeople, - ); + // during the second mount, the loading prop should be false, and data should + // be present; + if (count === 3) { + expect(this.props.data!.loading).toBeFalsy(); + expect(stripSymbols(this.props.data!.allPeople)).toEqual( + data.allPeople, + ); + } } - } - componentDidMount() { - if (count === 4) { - wrapper1.unmount(); - done(); + componentDidMount() { + if (count === 4) { + wrapper1.unmount(); + done(); + } } - } - componentDidUpdate(prevProps) { - if (count === 3) { - expect(prevProps.data.loading).toBeTruthy(); - expect(this.props.data.loading).toBeFalsy(); - expect(stripSymbols(this.props.data.allPeople)).toEqual( - data.allPeople, - ); + componentDidUpdate(prevProps: ChildProps<{}, Data>) { + if (count === 3) { + expect(prevProps.data!.loading).toBeTruthy(); + expect(this.props.data!.loading).toBeFalsy(); + expect(stripSymbols(this.props.data!.allPeople)).toEqual( + data.allPeople, + ); - // ensure first assertion and umount tree - assert1(); - wrapper1.find('#break').simulate('click'); + // ensure first assertion and umount tree + assert1(); + wrapper1.find('#break').simulate('click'); - // ensure cleanup - assert2(); + // ensure cleanup + assert2(); + } } - } - render() { - // side effect to keep track of render counts - count++; - return null; - } - } + render() { + // side effect to keep track of render counts + count++; + return null; + } + }, + ); - class RedirectOnMount extends React.Component { + class RedirectOnMount extends React.Component<{ onMount: () => void }> { componentWillMount() { this.props.onMount(); } @@ -111,18 +117,18 @@ describe('[queries] observableQuery', () => { } } - class AppWrapper extends React.Component { + class AppWrapper extends React.Component<{}, { renderRedirect: boolean }> { state = { renderRedirect: false, }; goToRedirect = () => { this.setState({ renderRedirect: true }); - }; // tslint:disable-line + }; handleRedirectMount = () => { this.setState({ renderRedirect: false }); - }; // tslint:disable-line + }; render() { if (this.state.renderRedirect) { @@ -148,7 +154,7 @@ describe('[queries] observableQuery', () => { }); it('will not try to refetch recycled `ObservableQuery`s when resetting the client store', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -157,13 +163,14 @@ describe('[queries] observableQuery', () => { } } `; + // const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; let finish = () => {}; // tslint:disable-line let called = 0; const link = new ApolloLink((o, f) => { called++; setTimeout(finish, 5); - return f(o); + return f ? f(o) : null; }).concat( mockSingleLink({ request: { query }, @@ -185,12 +192,13 @@ describe('[queries] observableQuery', () => { done(); }; - @graphql(query) - class Container extends React.Component { - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component { + render() { + return null; + } + }, + ); const wrapper1 = renderer.create( @@ -217,7 +225,7 @@ describe('[queries] observableQuery', () => { }); it('will recycle `ObservableQuery`s when re-rendering a portion of the tree', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -227,6 +235,7 @@ describe('[queries] observableQuery', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + const link = mockSingleLink( { request: { query }, result: { data } }, { request: { query }, result: { data } }, @@ -257,12 +266,13 @@ describe('[queries] observableQuery', () => { } } - @graphql(query) - class Container extends React.Component { - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component { + render() { + return null; + } + }, + ); const wrapper = renderer.create( @@ -302,7 +312,7 @@ describe('[queries] observableQuery', () => { }); it('will not recycle parallel GraphQL container `ObservableQuery`s', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -342,12 +352,13 @@ describe('[queries] observableQuery', () => { } } - @graphql(query) - class Container extends React.Component { - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component { + render() { + return null; + } + }, + ); const wrapper = renderer.create( @@ -394,7 +405,7 @@ describe('[queries] observableQuery', () => { }); it("will recycle `ObservableQuery`s when re-rendering a portion of the tree but not return stale data if variables don't match", done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int!) { allPeople(first: $first) { people { @@ -419,6 +430,9 @@ describe('[queries] observableQuery', () => { }, }; + type Data = typeof data; + type Vars = typeof variables1; + const link = mockSingleLink( { request: { query, variables: variables1 }, result: { data } }, { request: { query, variables: variables2 }, result: { data: data2 } }, @@ -429,7 +443,38 @@ describe('[queries] observableQuery', () => { }); let remount: any; - class Remounter extends React.Component { + const Container = graphql(query)( + class extends React.Component> { + render() { + try { + const { variables, loading, allPeople } = this.props.data!; + // first variable render + if (variables.first === 1) { + if (loading) expect(allPeople).toBeUndefined(); + if (!loading) { + expect(stripSymbols(allPeople)).toEqual(data.allPeople); + } + } + + if (variables.first === 2) { + // second variables render + if (loading) expect(allPeople).toBeUndefined(); + if (!loading) + expect(stripSymbols(allPeople)).toEqual(data2.allPeople); + } + } catch (e) { + done.fail(e); + } + + return null; + } + }, + ); + + class Remounter extends React.Component< + { render: typeof Container }, + { showChildren: boolean; variables: Vars } + > { state = { showChildren: true, variables: variables1, @@ -455,33 +500,6 @@ describe('[queries] observableQuery', () => { } } - @graphql(query) - class Container extends React.Component { - render() { - try { - const { variables, loading, allPeople } = this.props.data; - // first variable render - if (variables.first === 1) { - if (loading) expect(allPeople).toBeUndefined(); - if (!loading) { - expect(stripSymbols(allPeople)).toEqual(data.allPeople); - } - } - - if (variables.first === 2) { - // second variables render - if (loading) expect(allPeople).toBeUndefined(); - if (!loading) - expect(stripSymbols(allPeople)).toEqual(data2.allPeople); - } - } catch (e) { - done.fail(e); - } - - return null; - } - } - // the initial mount fires off the query // the same as episode id = 1 const wrapper = renderer.create( diff --git a/test/client/graphql/queries/polling.test.tsx b/test/client/graphql/queries/polling.test.tsx index cb6fd5b3f6..8e7e6b1f76 100644 --- a/test/client/graphql/queries/polling.test.tsx +++ b/test/client/graphql/queries/polling.test.tsx @@ -4,10 +4,11 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; +import { DocumentNode } from 'graphql'; describe('[queries] polling', () => { - let error; + let error: typeof console.error; beforeEach(() => { error = console.error; console.error = jest.fn(() => {}); // tslint:disable-line @@ -22,7 +23,7 @@ describe('[queries] polling', () => { const POLL_INTERVAL = 250; const POLL_COUNT = 4; - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -73,7 +74,7 @@ describe('[queries] polling', () => { }); it('exposes stopPolling as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -91,20 +92,19 @@ describe('[queries] polling', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // tslint:disable-line - expect(data.stopPolling).toBeTruthy(); - expect(data.stopPolling instanceof Function).toBeTruthy(); - expect(data.stopPolling).not.toThrow(); - done(); - } - render() { - return null; - } - } - + const Container = graphql(query)( + class extends React.Component { + componentWillReceiveProps({ data }: ChildProps) { + expect(data!.stopPolling).toBeTruthy(); + expect(data!.stopPolling instanceof Function).toBeTruthy(); + expect(data!.stopPolling).not.toThrow(); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -113,7 +113,7 @@ describe('[queries] polling', () => { }); it('exposes startPolling as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -130,24 +130,24 @@ describe('[queries] polling', () => { link, cache: new Cache({ addTypename: false }), }); - let wrapper; - @graphql(query, { options: { pollInterval: 10 } }) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // tslint:disable-line - expect(data.startPolling).toBeTruthy(); - expect(data.startPolling instanceof Function).toBeTruthy(); - // XXX this does throw because of no pollInterval - // expect(data.startPolling).not.toThrow(); - setTimeout(() => { - wrapper.unmount(); - done(); - }, 0); - } - render() { - return null; - } - } + let wrapper: renderer.ReactTestRenderer; + const Container = graphql(query, { options: { pollInterval: 10 } })( + class extends React.Component { + componentWillReceiveProps({ data }: ChildProps) { + expect(data!.startPolling).toBeTruthy(); + expect(data!.startPolling instanceof Function).toBeTruthy(); + // XXX this does throw because of no pollInterval + // expect(data.startPolling).not.toThrow(); + setTimeout(() => { + wrapper.unmount(); + done(); + }, 0); + } + render() { + return null; + } + }, + ); wrapper = renderer.create( diff --git a/test/client/graphql/queries/reducer.test.tsx b/test/client/graphql/queries/reducer.test.tsx index dc5c51d3c4..a8e6a68428 100644 --- a/test/client/graphql/queries/reducer.test.tsx +++ b/test/client/graphql/queries/reducer.test.tsx @@ -4,14 +4,15 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, ChildProps, graphql } from '../../../../src'; +import { ApolloProvider, graphql } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('[queries] reducer', () => { // props reducer it('allows custom mapping of a result to props', () => { - const query = gql` + const query: DocumentNode = gql` query thing { getThing { thing @@ -28,18 +29,19 @@ describe('[queries] reducer', () => { cache: new Cache({ addTypename: false }), }); - interface Props { - showSpinner: boolean; - } interface Data { getThing: { thing: boolean }; } - const ContainerWithData = graphql(query, { + interface FinalProps { + showSpinner: boolean | undefined; + } + + const ContainerWithData = graphql<{}, Data, {}, FinalProps>(query, { props: result => ({ showSpinner: result.data && result.data.loading, }), - })(({ showSpinner }) => { + })(({ showSpinner }: FinalProps) => { expect(showSpinner).toBeTruthy(); return null; }); @@ -53,7 +55,7 @@ describe('[queries] reducer', () => { }); it('allows custom mapping of a result to props that includes the passed props', () => { - const query = gql` + const query: DocumentNode = gql` query thing { getThing { thing @@ -72,16 +74,20 @@ describe('[queries] reducer', () => { interface Data { getThing: { thing: boolean }; } - type Props = { - showSpinner: boolean; + interface Props { sample: number; + } + + type FinalProps = { + showSpinner: boolean; }; - const ContainerWithData = graphql(query, { + + const ContainerWithData = graphql(query, { props: ({ data, ownProps }) => { expect(ownProps.sample).toBe(1); - return { showSpinner: data.loading }; + return { showSpinner: data!.loading }; }, - })(({ showSpinner }) => { + })(({ showSpinner }: FinalProps) => { expect(showSpinner).toBeTruthy(); return null; }); @@ -95,7 +101,7 @@ describe('[queries] reducer', () => { }); it('allows custom mapping of a result to props 2', done => { - const query = gql` + const query: DocumentNode = gql` query thing { getThing { thing @@ -112,19 +118,20 @@ describe('[queries] reducer', () => { cache: new Cache({ addTypename: false }), }); - interface Props { - thingy: boolean; - } interface Data { - getThing?: { thing: boolean }; + getThing: { thing: boolean }; + } + + interface FinalProps { + thingy: { thing: boolean }; } - const withData = graphql(query, { - props: ({ data }) => ({ thingy: data.getThing }), + const withData = graphql<{}, Data, {}, FinalProps>(query, { + props: ({ data }) => ({ thingy: data!.getThing! }), }); - class Container extends React.Component> { - componentWillReceiveProps(props: ChildProps) { + class Container extends React.Component { + componentWillReceiveProps(props: FinalProps) { expect(stripSymbols(props.thingy)).toEqual(expectedData.getThing); done(); } diff --git a/test/client/graphql/queries/skip.test.tsx b/test/client/graphql/queries/skip.test.tsx index 57838e7323..17750515d6 100644 --- a/test/client/graphql/queries/skip.test.tsx +++ b/test/client/graphql/queries/skip.test.tsx @@ -5,14 +5,15 @@ import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; import catchAsyncError from '../../../test-utils/catchAsyncError'; +import { DocumentNode } from 'graphql'; describe('[queries] skip', () => { it('allows you to skip a query without running it', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -30,19 +31,25 @@ describe('[queries] skip', () => { link, cache: new Cache({ addTypename: false }), }); - - let queryExecuted; - @graphql(query, { skip: ({ skip }) => skip }) - class Container extends React.Component { - componentWillReceiveProps(props) { - queryExecuted = true; - } - render() { - expect(this.props.data).toBeUndefined(); - return null; - } + interface Props { + skip: boolean; } + let queryExecuted = false; + const Container = graphql(query, { + skip: ({ skip }) => skip, + })( + class extends React.Component> { + componentWillReceiveProps() { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeUndefined(); + return null; + } + }, + ); + renderer.create( @@ -59,7 +66,7 @@ describe('[queries] skip', () => { }); it('continues to not subscribe to a skipped query when props change', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -68,9 +75,10 @@ describe('[queries] skip', () => { } } `; + const link = new ApolloLink((o, f) => { done.fail(new Error('query ran even though skip present')); - return f(o); + return f ? f(o) : null; }).concat(mockSingleLink()); // const oldQuery = link.query; const client = new ApolloClient({ @@ -78,17 +86,22 @@ describe('[queries] skip', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { skip: true }) - class Container extends React.Component { - componentWillReceiveProps(props) { - done(); - } - render() { - return null; - } + interface Props { + foo: number; } - class Parent extends React.Component { + const Container = graphql(query, { skip: true })( + class extends React.Component> { + componentWillReceiveProps() { + done(); + } + render() { + return null; + } + }, + ); + + class Parent extends React.Component<{}, { foo: number }> { state = { foo: 42 }; componentDidMount() { @@ -107,7 +120,7 @@ describe('[queries] skip', () => { }); it('supports using props for skipping which are used in options', done => { - const query = gql` + const query: DocumentNode = gql` query people($id: ID!) { allPeople(first: $id) { people { @@ -120,7 +133,12 @@ describe('[queries] skip', () => { const data = { allPeople: { people: { id: 1 } }, }; + + type Data = typeof data; + const variables = { id: 1 }; + type Vars = typeof variables; + const link = mockSingleLink({ request: { query, variables }, result: { data }, @@ -135,34 +153,38 @@ describe('[queries] skip', () => { let renderCount = 0; interface Props { - person: any; + person: { id: number } | null; } - @graphql(query, { + const Container = graphql(query, { skip: ({ person }) => !person, options: ({ person }) => ({ variables: { - id: person.id, + id: person!.id, }, }), - }) - class Container extends React.Component { - componentWillReceiveProps(props) { - count++; - if (count === 1) expect(props.data.loading).toBeTruthy(); - if (count === 2) - expect(stripSymbols(props.data.allPeople)).toEqual(data.allPeople); - if (count === 2) { - expect(renderCount).toBe(2); - done(); + })( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps) { + count++; + if (count === 1) expect(props.data!.loading).toBeTruthy(); + if (count === 2) + expect(stripSymbols(props.data!.allPeople)).toEqual(data.allPeople); + if (count === 2) { + expect(renderCount).toBe(2); + done(); + } } - } - render() { - renderCount++; - return null; - } - } + render() { + renderCount++; + return null; + } + }, + ); - class Parent extends React.Component { + class Parent extends React.Component< + {}, + { person: { id: number } | null } + > { state = { person: null }; componentDidMount() { @@ -181,7 +203,7 @@ describe('[queries] skip', () => { }); it("doesn't run options or props when skipped, including option.client", done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -200,15 +222,20 @@ describe('[queries] skip', () => { cache: new Cache({ addTypename: false }), }); - let queryExecuted; - let optionsCalled; + let queryExecuted = false; + let optionsCalled = false; interface Props { - pollInterval: number; skip: boolean; + pollInterval?: number; + } + + interface FinalProps { + pollInterval: number; + data?: {}; } - @graphql(query, { + const Container = graphql(query, { skip: ({ skip }) => skip, options: props => { optionsCalled = true; @@ -216,20 +243,21 @@ describe('[queries] skip', () => { pollInterval: props.pollInterval, }; }, - props: ({ willThrowIfAccesed }: any) => ({ + props: props => ({ // intentionally incorrect - pollInterval: willThrowIfAccesed.pollInterval, + pollInterval: (props as any).willThrowIfAccesed.pollInterval, }), - }) - class Container extends React.Component { - componentWillReceiveProps(props) { - queryExecuted = true; - } - render() { - expect(this.props.data).toBeFalsy(); - return null; - } - } + })( + class extends React.Component { + componentWillReceiveProps() { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeFalsy(); + return null; + } + }, + ); renderer.create( @@ -243,7 +271,7 @@ describe('[queries] skip', () => { return; } if (optionsCalled) { - fail(new Error('options ruan even through skip present')); + fail(new Error('options ran even though skip present')); return; } fail(new Error('query ran even though skip present')); @@ -251,7 +279,7 @@ describe('[queries] skip', () => { }); it("doesn't run options or props when skipped even if the component updates", done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -273,7 +301,10 @@ describe('[queries] skip', () => { let queryWasSkipped = true; - @graphql(query, { + interface Props { + foo: string; + } + const Container = graphql(query, { skip: true, options: () => { queryWasSkipped = false; @@ -283,18 +314,19 @@ describe('[queries] skip', () => { queryWasSkipped = false; return {}; }, - }) - class Container extends React.Component { - componentWillReceiveProps(props) { - expect(queryWasSkipped).toBeTruthy(); - done(); - } - render() { - return null; - } - } + })( + class extends React.Component> { + componentWillReceiveProps() { + expect(queryWasSkipped).toBeTruthy(); + done(); + } + render() { + return null; + } + }, + ); - class Parent extends React.Component { + class Parent extends React.Component<{}, { foo: string }> { state = { foo: 'bar' }; componentDidMount() { this.setState({ foo: 'baz' }); @@ -312,7 +344,7 @@ describe('[queries] skip', () => { }); it('allows you to skip a query without running it (alternate syntax)', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -331,17 +363,18 @@ describe('[queries] skip', () => { cache: new Cache({ addTypename: false }), }); - let queryExecuted; - @graphql(query, { skip: true }) - class Container extends React.Component { - componentWillReceiveProps(props) { - queryExecuted = true; - } - render() { - expect(this.props.data).toBeFalsy(); - return null; - } - } + let queryExecuted = false; + const Container = graphql(query, { skip: true })( + class extends React.Component { + componentWillReceiveProps() { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeFalsy(); + return null; + } + }, + ); renderer.create( @@ -361,7 +394,7 @@ describe('[queries] skip', () => { // test the case of skip:false -> skip:true -> skip:false to make sure things // are cleaned up properly it('allows you to skip then unskip a query with top-level syntax', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -381,25 +414,31 @@ describe('[queries] skip', () => { }); let hasSkipped = false; - @graphql(query, { skip: ({ skip }) => skip }) - class Container extends React.Component { - 8; - componentWillReceiveProps(newProps) { - if (newProps.skip) { - hasSkipped = true; - this.props.setSkip(false); - } else { - if (hasSkipped) { - done(); + + interface Props { + skip: boolean; + setSkip: (skip: boolean) => void; + } + + const Container = graphql(query, { skip: ({ skip }) => skip })( + class extends React.Component> { + componentWillReceiveProps(newProps: ChildProps) { + if (newProps.skip) { + hasSkipped = true; + this.props.setSkip(false); } else { - this.props.setSkip(true); + if (hasSkipped) { + done(); + } else { + this.props.setSkip(true); + } } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); class Parent extends React.Component { state = { skip: false }; @@ -421,7 +460,7 @@ describe('[queries] skip', () => { }); it('allows you to skip then unskip a query with new options (top-level syntax)', done => { - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -432,6 +471,10 @@ describe('[queries] skip', () => { `; const dataOne = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; const dataTwo = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; + + type Data = typeof dataOne; + type Vars = { first: number }; + const link = mockSingleLink( { request: { query, variables: { first: 1 } }, @@ -452,36 +495,46 @@ describe('[queries] skip', () => { }); let hasSkipped = false; - @graphql(query, { skip: ({ skip }) => skip }) - class Container extends React.Component { - 8; - componentWillReceiveProps(newProps) { - if (newProps.skip) { - hasSkipped = true; - // change back to skip: false, with a different variable - this.props.setState({ skip: false, first: 2 }); - } else { - if (hasSkipped) { - if (!newProps.data.loading) { - expect(stripSymbols(newProps.data.allPeople)).toEqual( - dataTwo.allPeople, + + interface Props { + skip: boolean; + first: number; + setState: ( + state: Pick<{ skip: boolean; first: number }, K>, + ) => void; + } + const Container = graphql(query, { + skip: ({ skip }) => skip, + })( + class extends React.Component> { + componentWillReceiveProps(newProps: ChildProps) { + if (newProps.skip) { + hasSkipped = true; + // change back to skip: false, with a different variable + this.props.setState({ skip: false, first: 2 }); + } else { + if (hasSkipped) { + if (!newProps.data!.loading) { + expect(stripSymbols(newProps.data!.allPeople)).toEqual( + dataTwo.allPeople, + ); + done(); + } + } else { + expect(stripSymbols(newProps.data!.allPeople)).toEqual( + dataOne.allPeople, ); - done(); + this.props.setState({ skip: true }); } - } else { - expect(stripSymbols(newProps.data.allPeople)).toEqual( - dataOne.allPeople, - ); - this.props.setState({ skip: true }); } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); - class Parent extends React.Component { + class Parent extends React.Component<{}, { skip: boolean; first: number }> { state = { skip: false, first: 1 }; render() { return ( @@ -502,7 +555,7 @@ describe('[queries] skip', () => { }); it('allows you to skip then unskip a query with opts syntax', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -516,7 +569,7 @@ describe('[queries] skip', () => { let ranQuery = 0; const link = new ApolloLink((o, f) => { ranQuery++; - return f(o); + return f ? f(o) : null; }).concat( mockSingleLink({ request: { query }, @@ -535,39 +588,41 @@ describe('[queries] skip', () => { interface Props { skip: boolean; + setSkip: (skip: boolean) => void; } - @graphql(query, { + const Container = graphql(query, { options: () => ({ fetchPolicy: 'network-only' }), skip: ({ skip }) => skip, - }) - class Container extends React.Component { - componentWillReceiveProps(newProps) { - if (newProps.skip) { - // Step 2. We shouldn't query again. - expect(ranQuery).toBe(1); - hasSkipped = true; - this.props.setSkip(false); - } else if (hasRequeried) { - // Step 4. We need to actually get the data from the query into the component! - expect(newProps.data.loading).toBeFalsy(); - done(); - } else if (hasSkipped) { - // Step 3. We need to query again! - expect(newProps.data.loading).toBeTruthy(); - expect(ranQuery).toBe(2); - hasRequeried = true; - } else { - // Step 1. We've queried once. - expect(ranQuery).toBe(1); - this.props.setSkip(true); + })( + class extends React.Component> { + componentWillReceiveProps(newProps: ChildProps) { + if (newProps.skip) { + // Step 2. We shouldn't query again. + expect(ranQuery).toBe(1); + hasSkipped = true; + this.props.setSkip(false); + } else if (hasRequeried) { + // Step 4. We need to actually get the data from the query into the component! + expect(newProps.data!.loading).toBeFalsy(); + done(); + } else if (hasSkipped) { + // Step 3. We need to query again! + expect(newProps.data!.loading).toBeTruthy(); + expect(ranQuery).toBe(2); + hasRequeried = true; + } else { + // Step 1. We've queried once. + expect(ranQuery).toBe(1); + this.props.setSkip(true); + } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); - class Parent extends React.Component { + class Parent extends React.Component<{}, { skip: boolean }> { state = { skip: false }; render() { return ( @@ -588,7 +643,7 @@ describe('[queries] skip', () => { it('removes the injected props if skip becomes true', done => { let count = 0; - const query = gql` + const query: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -607,6 +662,9 @@ describe('[queries] skip', () => { const data3 = { allPeople: { people: [{ name: 'Anakin Skywalker' }] } }; const variables3 = { first: 3 }; + type Data = typeof data1; + type Vars = typeof variables1; + const link = mockSingleLink( { request: { query, variables: variables1 }, result: { data: data1 } }, { request: { query, variables: variables2 }, result: { data: data2 } }, @@ -618,28 +676,29 @@ describe('[queries] skip', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { + const Container = graphql(query, { skip: () => count === 1, - }) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - catchAsyncError(done, () => { - // loading is true, but data still there - if (count === 0) - expect(stripSymbols(data.allPeople)).toEqual(data1.allPeople); - if (count === 1) expect(data).toBeUndefined(); - if (count === 2 && !data.loading) { - expect(stripSymbols(data.allPeople)).toEqual(data3.allPeople); - done(); - } - }); - } - render() { - return null; - } - } + })( + class extends React.Component> { + componentWillReceiveProps({ data }: ChildProps) { + catchAsyncError(done, () => { + // loading is true, but data still there + if (count === 0) + expect(stripSymbols(data!.allPeople)).toEqual(data1.allPeople); + if (count === 1) expect(data).toBeUndefined(); + if (count === 2 && !data!.loading) { + expect(stripSymbols(data!.allPeople)).toEqual(data3.allPeople); + done(); + } + }); + } + render() { + return null; + } + }, + ); - class ChangingProps extends React.Component { + class ChangingProps extends React.Component<{}, { first: number }> { state = { first: 1 }; componentDidMount() { setTimeout(() => { @@ -666,7 +725,7 @@ describe('[queries] skip', () => { }); it('allows you to unmount a skipped query', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -681,22 +740,27 @@ describe('[queries] skip', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query, { - skip: true, - }) - class Container extends React.Component { - componentDidMount() { - this.props.hide(); - } - componentWillUnmount() { - done(); - } - render() { - return null; - } + interface Props { + hide: () => void; } - class Hider extends React.Component { + const Container = graphql(query, { + skip: true, + })( + class extends React.Component> { + componentDidMount() { + this.props.hide(); + } + componentWillUnmount() { + done(); + } + render() { + return null; + } + }, + ); + + class Hider extends React.Component<{}, { hide: boolean }> { state = { hide: false }; render() { if (this.state.hide) { diff --git a/test/client/graphql/queries/updateQuery.test.tsx b/test/client/graphql/queries/updateQuery.test.tsx index 6d38825455..a42595a124 100644 --- a/test/client/graphql/queries/updateQuery.test.tsx +++ b/test/client/graphql/queries/updateQuery.test.tsx @@ -4,14 +4,15 @@ import gql from 'graphql-tag'; import ApolloClient from 'apollo-client'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../../../src/test-utils'; -import { ApolloProvider, graphql } from '../../../../src'; +import { ApolloProvider, graphql, ChildProps } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('[queries] updateQuery', () => { // updateQuery it('exposes updateQuery as part of the props api', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -29,22 +30,22 @@ describe('[queries] updateQuery', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps({ data }) { - // tslint:disable-line - expect(data.updateQuery).toBeTruthy(); - expect(data.updateQuery instanceof Function).toBeTruthy(); - try { - data.updateQuery(() => done()); - } catch (error) { - // fail + const Container = graphql(query)( + class extends React.Component { + componentWillReceiveProps({ data }: ChildProps) { + expect(data!.updateQuery).toBeTruthy(); + expect(data!.updateQuery instanceof Function).toBeTruthy(); + try { + data!.updateQuery(() => done()); + } catch (error) { + // fail + } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( @@ -54,7 +55,7 @@ describe('[queries] updateQuery', () => { }); it('exposes updateQuery as part of the props api during componentWillMount', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -72,17 +73,18 @@ describe('[queries] updateQuery', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillMount() { - expect(this.props.data.updateQuery).toBeTruthy(); - expect(this.props.data.updateQuery instanceof Function).toBeTruthy(); - done(); - } - render() { - return null; - } - } + const Container = graphql(query)( + class extends React.Component { + componentWillMount() { + expect(this.props.data!.updateQuery).toBeTruthy(); + expect(this.props.data!.updateQuery instanceof Function).toBeTruthy(); + done(); + } + render() { + return null; + } + }, + ); renderer.create( @@ -92,7 +94,7 @@ describe('[queries] updateQuery', () => { }); it('updateQuery throws if called before data has returned', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -110,25 +112,26 @@ describe('[queries] updateQuery', () => { cache: new Cache({ addTypename: false }), }); - @graphql(query) - class Container extends React.Component { - componentWillMount() { - expect(this.props.data.updateQuery).toBeTruthy(); - expect(this.props.data.updateQuery instanceof Function).toBeTruthy(); - try { - this.props.data.updateQuery(); - done(); - } catch (e) { - expect(e.toString()).toMatch( - /ObservableQuery with this id doesn't exist:/, - ); - done(); + const Container = graphql(query)( + class extends React.Component { + componentWillMount() { + expect(this.props.data!.updateQuery).toBeTruthy(); + expect(this.props.data!.updateQuery instanceof Function).toBeTruthy(); + try { + this.props.data!.updateQuery(p => p); + done(); + } catch (e) { + expect(e.toString()).toMatch( + /ObservableQuery with this id doesn't exist:/, + ); + done(); + } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( @@ -138,7 +141,7 @@ describe('[queries] updateQuery', () => { }); it('allows updating query results after query has finished (early binding)', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -148,6 +151,7 @@ describe('[queries] updateQuery', () => { } `; const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const link = mockSingleLink( { request: { query }, result: { data: data1 } }, @@ -158,29 +162,32 @@ describe('[queries] updateQuery', () => { cache: new Cache({ addTypename: false }), }); - let isUpdated; - @graphql(query) - class Container extends React.Component { - public updateQuery: any; - componentWillMount() { - this.updateQuery = this.props.data.updateQuery; - } - componentWillReceiveProps(props) { - if (isUpdated) { - expect(stripSymbols(props.data.allPeople)).toEqual(data2.allPeople); - done(); - return; - } else { - isUpdated = true; - this.updateQuery(prev => { - return data2; - }); + let isUpdated = false; + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + public updateQuery: any; + componentWillMount() { + this.updateQuery = this.props.data!.updateQuery; } - } - render() { - return null; - } - } + componentWillReceiveProps(props: ChildProps<{}, Data>) { + if (isUpdated) { + expect(stripSymbols(props.data!.allPeople)).toEqual( + data2.allPeople, + ); + done(); + return; + } else { + isUpdated = true; + this.updateQuery(() => { + return data2; + }); + } + } + render() { + return null; + } + }, + ); renderer.create( @@ -190,7 +197,7 @@ describe('[queries] updateQuery', () => { }); it('allows updating query results after query has finished', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -200,6 +207,8 @@ describe('[queries] updateQuery', () => { } `; const data1 = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data1; + const data2 = { allPeople: { people: [{ name: 'Leia Skywalker' }] } }; const link = mockSingleLink( { request: { query }, result: { data: data1 } }, @@ -210,25 +219,28 @@ describe('[queries] updateQuery', () => { cache: new Cache({ addTypename: false }), }); - let isUpdated; - @graphql(query) - class Container extends React.Component { - componentWillReceiveProps(props) { - if (isUpdated) { - expect(stripSymbols(props.data.allPeople)).toEqual(data2.allPeople); - done(); - return; - } else { - isUpdated = true; - props.data.updateQuery(prev => { - return data2; - }); + let isUpdated = false; + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillReceiveProps(props: ChildProps<{}, Data>) { + if (isUpdated) { + expect(stripSymbols(props.data!.allPeople)).toEqual( + data2.allPeople, + ); + done(); + return; + } else { + isUpdated = true; + props.data!.updateQuery(() => { + return data2; + }); + } } - } - render() { - return null; - } - } + render() { + return null; + } + }, + ); renderer.create( diff --git a/test/client/graphql/shared-operations.test.tsx b/test/client/graphql/shared-operations.test.tsx index 93081f99dd..a27ef9c0f2 100644 --- a/test/client/graphql/shared-operations.test.tsx +++ b/test/client/graphql/shared-operations.test.tsx @@ -13,20 +13,20 @@ import { withApollo, } from '../../../src'; import * as TestUtils from 'react-dom/test-utils'; - -const compose = require('lodash/flowRight'); +import { DocumentNode } from 'graphql'; +import compose from 'lodash/flowRight'; describe('shared operations', () => { describe('withApollo', () => { it('passes apollo-client to props', () => { const client = new ApolloClient({ - link: new ApolloLink((o, f) => f(o)), + link: new ApolloLink((o, f) => (f ? f(o) : null)), cache: new Cache(), }); @withApollo class ContainerWithData extends React.Component { - render() { + render(): React.ReactNode { expect(this.props.client).toEqual(client); return null; } @@ -41,7 +41,7 @@ describe('shared operations', () => { it('allows a way to access the wrapped component instance', () => { const client = new ApolloClient({ - link: new ApolloLink((o, f) => f(o)), + link: new ApolloLink((o, f) => (f ? f(o) : null)), cache: new Cache(), }); @@ -103,7 +103,7 @@ describe('shared operations', () => { }); it('binds two queries to props', () => { - const peopleQuery = gql` + const peopleQuery: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -117,7 +117,7 @@ describe('shared operations', () => { allPeople: { people: [{ name: string }] }; } - const shipsQuery = gql` + const shipsQuery: DocumentNode = gql` query ships { allships(first: 1) { ships { @@ -140,21 +140,32 @@ describe('shared operations', () => { cache: new Cache({ addTypename: false }), }); - const withPeople = graphql<{}, PeopleData>(peopleQuery, { - name: 'people', - }); - const withShips = graphql<{}, ShipsData>(shipsQuery, { name: 'ships' }); + interface PeopleChildProps { + people: DataValue; + } - interface ComposedProps { - people?: DataValue; - ships?: DataValue; + // Since we want to test decorators usage, and this does not play well with Typescript, + // we resort to setting everything as any to avoid type checking. + const withPeople: any = graphql<{}, PeopleData, {}, PeopleChildProps>( + peopleQuery, + { + name: 'people', + }, + ); + + interface ShipsChildProps { + ships: DataValue; } + const withShips: any = graphql<{}, ShipsData, {}, ShipsChildProps>( + shipsQuery, + { + name: 'ships', + }, + ); @withPeople @withShips - class ContainerWithData extends React.Component< - ChildProps - > { + class ContainerWithData extends React.Component { render() { const { people, ships } = this.props; expect(people).toBeTruthy(); @@ -175,7 +186,7 @@ describe('shared operations', () => { }); it('binds two queries to props with different syntax', () => { - const peopleQuery = gql` + const peopleQuery: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -188,7 +199,7 @@ describe('shared operations', () => { interface PeopleData { allPeople: { people: [{ name: string }] }; } - const shipsQuery = gql` + const shipsQuery: DocumentNode = gql` query ships { allships(first: 1) { ships { @@ -211,17 +222,31 @@ describe('shared operations', () => { cache: new Cache({ addTypename: false }), }); - const withPeople = graphql(peopleQuery, { - name: 'people', - }); - const withShips = graphql(shipsQuery, { name: 'ships' }); + interface PeopleChildProps { + people: DataValue; + } - interface ComposedProps { - people?: DataValue; - ships?: DataValue; + const withPeople = graphql<{}, PeopleData, {}, PeopleChildProps>( + peopleQuery, + { + name: 'people', + }, + ); + + interface ShipsAndPeopleChildProps extends PeopleChildProps { + ships: DataValue; } + const withShips = graphql< + PeopleChildProps, + ShipsData, + {}, + ShipsAndPeopleChildProps + >(shipsQuery, { + name: 'ships', + }); + const ContainerWithData = withPeople( - withShips((props: ComposedProps) => { + withShips((props: ShipsAndPeopleChildProps) => { const { people, ships } = props; expect(people).toBeTruthy(); expect(people.loading).toBeTruthy(); @@ -241,7 +266,7 @@ describe('shared operations', () => { }); it('binds two operations to props', () => { - const peopleQuery = gql` + const peopleQuery: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -252,7 +277,7 @@ describe('shared operations', () => { `; const peopleData = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; - const peopleMutation = gql` + const peopleMutation: DocumentNode = gql` mutation addPerson { allPeople(first: 1) { people { @@ -280,18 +305,20 @@ describe('shared operations', () => { const withPeople = graphql(peopleQuery, { name: 'people' }); const withPeopleMutation = graphql(peopleMutation, { name: 'addPerson' }); - @withPeople - @withPeopleMutation - class ContainerWithData extends React.Component { - render() { - const { people, addPerson } = this.props; - expect(people).toBeTruthy(); - expect(people.loading).toBeTruthy(); - - expect(addPerson).toBeTruthy(); - return null; - } - } + const ContainerWithData = withPeople( + withPeopleMutation( + class extends React.Component { + render() { + const { people, addPerson } = this.props; + expect(people).toBeTruthy(); + expect(people.loading).toBeTruthy(); + + expect(addPerson).toBeTruthy(); + return null; + } + }, + ), + ); const wrapper = renderer.create( @@ -302,7 +329,7 @@ describe('shared operations', () => { }); it('allows a way to access the wrapped component instance', () => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -374,7 +401,7 @@ describe('shared operations', () => { }); it('allows options to take an object', done => { - const query = gql` + const query: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -384,6 +411,8 @@ describe('shared operations', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -393,17 +422,18 @@ describe('shared operations', () => { cache: new Cache({ addTypename: false }), }); - let queryExecuted; - @graphql(query, { skip: true }) - class Container extends React.Component { - componentWillReceiveProps(props) { - queryExecuted = true; - } - render() { - expect(this.props.data).toBeUndefined(); - return null; - } - } + let queryExecuted = false; + const Container = graphql<{}, Data>(query, { skip: true })( + class extends React.Component> { + componentWillReceiveProps() { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeUndefined(); + return null; + } + }, + ); renderer.create( @@ -422,7 +452,7 @@ describe('shared operations', () => { describe('compose', () => { it('binds two queries to props with different syntax', () => { - const peopleQuery = gql` + const peopleQuery: DocumentNode = gql` query people { allPeople(first: 1) { people { @@ -435,7 +465,9 @@ describe('shared operations', () => { allPeople: { people: [{ name: 'Luke Skywalker' }] }, }; - const shipsQuery = gql` + type PeopleData = typeof peopleData; + + const shipsQuery: DocumentNode = gql` query ships { allships(first: 1) { ships { @@ -446,6 +478,8 @@ describe('shared operations', () => { `; const shipsData = { allships: { ships: [{ name: 'Tie Fighter' }] } }; + type ShipsData = typeof shipsData; + const link = mockSingleLink( { request: { query: peopleQuery }, result: { data: peopleData } }, { request: { query: shipsQuery }, result: { data: shipsData } }, @@ -455,9 +489,23 @@ describe('shared operations', () => { cache: new Cache({ addTypename: false }), }); + interface PeopleChildProps { + people: DataValue; + } + + interface ShipsAndPeopleChildProps { + people: DataValue; + ships: DataValue; + } + const enhanced = compose( - graphql(peopleQuery, { name: 'people' }), - graphql(shipsQuery, { name: 'ships' }), + graphql<{}, PeopleData, {}, PeopleChildProps>(peopleQuery, { + name: 'people', + }), + graphql( + shipsQuery, + { name: 'ships' }, + ), ); const ContainerWithData = enhanced(props => { diff --git a/test/client/graphql/statics.test.tsx b/test/client/graphql/statics.test.tsx index 36be24fc67..77c72de490 100644 --- a/test/client/graphql/statics.test.tsx +++ b/test/client/graphql/statics.test.tsx @@ -12,12 +12,13 @@ let sampleOperation = gql` describe('statics', () => { it('should be preserved', () => { - @graphql(sampleOperation) - class ApolloContainer extends React.Component { - static veryStatic = 'such global'; - } + const ApolloContainer = graphql(sampleOperation)( + class extends React.Component { + static veryStatic = 'such global'; + }, + ); - expect(ApolloContainer.veryStatic).toBe('such global'); + expect((ApolloContainer as any).veryStatic).toBe('such global'); }); it('exposes a debuggable displayName', () => { @@ -30,10 +31,11 @@ describe('statics', () => { }); it('honors custom display names', () => { - @graphql(sampleOperation) - class ApolloContainer extends React.Component { - static displayName = 'Foo'; - } + const ApolloContainer = graphql(sampleOperation)( + class extends React.Component { + static displayName = 'Foo'; + }, + ); expect((ApolloContainer as any).displayName).toBe('Apollo(Foo)'); }); diff --git a/test/client/graphql/subscriptions.test.tsx b/test/client/graphql/subscriptions.test.tsx index df7b480f01..146622eda1 100644 --- a/test/client/graphql/subscriptions.test.tsx +++ b/test/client/graphql/subscriptions.test.tsx @@ -7,10 +7,11 @@ import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { MockSubscriptionLink } from '../../../src/test-utils'; import { ApolloProvider, ChildProps, graphql } from '../../../src'; import stripSymbols from '../../test-utils/stripSymbols'; +import { DocumentNode } from 'graphql'; describe('subscriptions', () => { - let error; - let wrapper; + let error: typeof console.error; + let wrapper: renderer.ReactTestRenderer | null; beforeEach(() => { jest.useRealTimers(); error = console.error; @@ -35,7 +36,7 @@ describe('subscriptions', () => { })); it('binds a subscription to props', () => { - const query = gql` + const query: DocumentNode = gql` subscription UserInfo { user { name @@ -56,8 +57,8 @@ describe('subscriptions', () => { const ContainerWithData = graphql(query)( ({ data }: ChildProps) => { expect(data).toBeTruthy(); - expect(data.user).toBeFalsy(); - expect(data.loading).toBeTruthy(); + expect(data!.user).toBeFalsy(); + expect(data!.loading).toBeTruthy(); return null; }, ); @@ -70,7 +71,7 @@ describe('subscriptions', () => { }); it('includes the variables in the props', () => { - const query = gql` + const query: DocumentNode = gql` subscription UserInfo($name: String) { user(name: $name) { name @@ -84,15 +85,18 @@ describe('subscriptions', () => { cache: new Cache({ addTypename: false }), }); - interface Props {} + interface Variables { + name: string; + } + interface Data { user: { name: string }; } - const ContainerWithData = graphql(query)( - ({ data }: ChildProps) => { + const ContainerWithData = graphql(query)( + ({ data }: ChildProps) => { expect(data).toBeTruthy(); - expect(data.variables).toEqual(variables); + expect(data!.variables).toEqual(variables); return null; }, ); @@ -105,7 +109,7 @@ describe('subscriptions', () => { }); it('does not swallow children errors', done => { - const query = gql` + const query: DocumentNode = gql` subscription UserInfo { user { name @@ -118,14 +122,14 @@ describe('subscriptions', () => { cache: new Cache({ addTypename: false }), }); - let bar; + let bar: any; const ContainerWithData = graphql(query)(() => { bar(); // this will throw return null; }); class ErrorBoundary extends React.Component { - componentDidCatch(e, info) { + componentDidCatch(e: any) { expect(e.name).toMatch(/TypeError/); expect(e.message).toMatch(/bar is not a function/); done(); @@ -148,13 +152,17 @@ describe('subscriptions', () => { it('executes a subscription', done => { jest.useFakeTimers(); - const query = gql` + const query: DocumentNode = gql` subscription UserInfo { user { name } } `; + + interface Data { + user: { name: string }; + } const link = new MockSubscriptionLink(); const client = new ApolloClient({ link, @@ -162,29 +170,32 @@ describe('subscriptions', () => { }); let count = 0; - @graphql(query) - class Container extends React.Component { - componentWillMount() { - expect(this.props.data.loading).toBeTruthy(); - } - componentWillReceiveProps({ data: { loading, user } }) { - expect(loading).toBeFalsy(); - if (count === 0) - expect(stripSymbols(user)).toEqual(results[0].result.data.user); - if (count === 1) - expect(stripSymbols(user)).toEqual(results[1].result.data.user); - if (count === 2) - expect(stripSymbols(user)).toEqual(results[2].result.data.user); - if (count === 3) { - expect(stripSymbols(user)).toEqual(results[3].result.data.user); - done(); + const Container = graphql<{}, Data>(query)( + class extends React.Component> { + componentWillMount() { + expect(this.props.data!.loading).toBeTruthy(); } - count++; - } - render() { - return null; - } - } + componentWillReceiveProps(props: ChildProps<{}, Data>) { + const { loading, user } = props.data!; + + expect(loading).toBeFalsy(); + if (count === 0) + expect(stripSymbols(user)).toEqual(results[0].result.data.user); + if (count === 1) + expect(stripSymbols(user)).toEqual(results[1].result.data.user); + if (count === 2) + expect(stripSymbols(user)).toEqual(results[2].result.data.user); + if (count === 3) { + expect(stripSymbols(user)).toEqual(results[3].result.data.user); + done(); + } + count++; + } + render() { + return null; + } + }, + ); const interval = setInterval(() => { link.simulateResult(results[count]); @@ -226,25 +237,29 @@ describe('subscriptions', () => { delay: 10, })); - const query = gql` + const query: DocumentNode = gql` subscription UserInfo { user { name } } `; - const triggerQuery = gql` + interface QueryData { + user: { name: string }; + } + + const triggerQuery: DocumentNode = gql` subscription Trigger { trigger } `; interface TriggerData { - trigger: any; + trigger: string; } const userLink = new MockSubscriptionLink(); const triggerLink = new MockSubscriptionLink(); - const link = new ApolloLink((o, f) => f(o)).split( + const link = new ApolloLink((o, f) => (f ? f(o) : null)).split( ({ operationName }) => operationName === 'UserInfo', userLink, triggerLink, @@ -256,44 +271,55 @@ describe('subscriptions', () => { }); let count = 0; - @graphql(triggerQuery) - @graphql<{}, TriggerData>(query, { - shouldResubscribe: (props, nextProps) => { - return nextProps.data.trigger === 'trigger resubscribe'; - }, - }) - class Container extends React.Component { - componentWillMount() { - expect(this.props.data.loading).toBeTruthy(); - } - componentWillReceiveProps({ data: { loading, user } }) { - try { - // odd counts will be outer wrapper getting subscriptions - ie unchanged - expect(loading).toBeFalsy(); - if (count === 0) - expect(stripSymbols(user)).toEqual(results[0].result.data.user); - if (count === 1) - expect(stripSymbols(user)).toEqual(results[0].result.data.user); - if (count === 2) - expect(stripSymbols(user)).toEqual(results[2].result.data.user); - if (count === 3) - expect(stripSymbols(user)).toEqual(results[2].result.data.user); - if (count === 4) - expect(stripSymbols(user)).toEqual(results3[2].result.data.user); - if (count === 5) { - expect(stripSymbols(user)).toEqual(results3[2].result.data.user); - done(); + + type TriggerQueryChildProps = ChildProps<{}, TriggerData>; + type ComposedProps = ChildProps; + + const Container = graphql<{}, TriggerData>(triggerQuery)( + graphql(query, { + shouldResubscribe: nextProps => { + return nextProps.data!.trigger === 'trigger resubscribe'; + }, + })( + class extends React.Component { + componentWillMount() { + expect(this.props.data!.loading).toBeTruthy(); } - } catch (e) { - done.fail(e); - } + componentWillReceiveProps(props: ComposedProps) { + const { loading, user } = props.data!; + try { + // odd counts will be outer wrapper getting subscriptions - ie unchanged + expect(loading).toBeFalsy(); + if (count === 0) + expect(stripSymbols(user)).toEqual(results[0].result.data.user); + if (count === 1) + expect(stripSymbols(user)).toEqual(results[0].result.data.user); + if (count === 2) + expect(stripSymbols(user)).toEqual(results[2].result.data.user); + if (count === 3) + expect(stripSymbols(user)).toEqual(results[2].result.data.user); + if (count === 4) + expect(stripSymbols(user)).toEqual( + results3[2].result.data.user, + ); + if (count === 5) { + expect(stripSymbols(user)).toEqual( + results3[2].result.data.user, + ); + done(); + } + } catch (e) { + done.fail(e); + } - count++; - } - render() { - return null; - } - } + count++; + } + render() { + return null; + } + }, + ), + ); const interval = setInterval(() => { try { diff --git a/test/server/getDataFromTree.test.tsx b/test/server/getDataFromTree.test.tsx index 90e5ff52c6..e77c68f0d5 100644 --- a/test/server/getDataFromTree.test.tsx +++ b/test/server/getDataFromTree.test.tsx @@ -6,15 +6,16 @@ import { graphql, Query, ApolloProvider, - DataValue, walkTree, getDataFromTree, + DataValue, } from '../../src'; import gql from 'graphql-tag'; import * as _ from 'lodash'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../../src/test-utils'; import { ChildProps } from '../../src/types'; +import { DocumentNode } from 'graphql'; describe('SSR', () => { describe('`walkTree`', () => { @@ -27,7 +28,7 @@ describe('SSR', () => { Bar
); - walkTree(rootElement, {}, element => { + walkTree(rootElement, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(5); @@ -36,7 +37,7 @@ describe('SSR', () => { it('basic element trees with nulls', () => { let elementCount = 0; const rootElement =
{null}
; - walkTree(rootElement, {}, element => { + walkTree(rootElement, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -45,7 +46,7 @@ describe('SSR', () => { it('basic element trees with false', () => { let elementCount = 0; const rootElement =
{false}
; - walkTree(rootElement, {}, element => { + walkTree(rootElement, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -54,7 +55,7 @@ describe('SSR', () => { it('basic element trees with empty string', () => { let elementCount = 0; const rootElement =
{''}
; - walkTree(rootElement, {}, element => { + walkTree(rootElement, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -63,7 +64,7 @@ describe('SSR', () => { it('basic element trees with arrays', () => { let elementCount = 0; const rootElement = [1, 2]; - walkTree(rootElement, {}, element => { + walkTree(rootElement, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -72,7 +73,7 @@ describe('SSR', () => { it('basic element trees with false or null', () => { let elementCount = 0; const rootElement = [1, false, null, '']; - walkTree(rootElement, {}, element => { + walkTree(rootElement, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -80,10 +81,10 @@ describe('SSR', () => { it('functional stateless components', () => { let elementCount = 0; - const MyComponent = ({ n }) => ( + const MyComponent = ({ n }: { n: number }) => (
{_.times(n, i => )}
); - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -126,13 +127,19 @@ describe('SSR', () => { it('functional stateless components with null children', () => { let elementCount = 0; - const MyComponent = ({ n, children = null }) => ( + const MyComponent = ({ + n, + children = null, + }: { + n: number; + children: React.ReactNode; + }) => (
{_.times(n, i => )} {children}
); - walkTree({null}, {}, element => { + walkTree({null}, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -141,7 +148,7 @@ describe('SSR', () => { it('functional stateless components that render null', () => { let elementCount = 0; const MyComponent = () => null; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -150,7 +157,7 @@ describe('SSR', () => { it('functional stateless components that render an array', () => { let elementCount = 0; const MyComponent = () => [1, 2] as any; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(3); @@ -160,7 +167,7 @@ describe('SSR', () => { let elementCount = 0; const MyComponent = () => [null,
] as any; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -170,7 +177,7 @@ describe('SSR', () => { let elementCount = 0; const MyComponent = () => [undefined,
] as any; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -183,7 +190,7 @@ describe('SSR', () => { return
{_.times(this.props.n, i => )}
; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -196,7 +203,7 @@ describe('SSR', () => { return null; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -209,7 +216,7 @@ describe('SSR', () => { return [1, 2]; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(3); @@ -223,7 +230,7 @@ describe('SSR', () => { return [null,
]; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -239,7 +246,7 @@ describe('SSR', () => { return
{_.times(this.props.n, i => )}
; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -262,7 +269,7 @@ describe('SSR', () => { Foo , {}, - element => { + () => { elementCount += 1; }, ); @@ -274,10 +281,10 @@ describe('SSR', () => { class MyComponent extends (React.Component as any) { render = () => { return
{_.times(this.props.n, i => )}
; - } + }; } const MyCompAsAny = MyComponent as any; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -311,9 +318,13 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query)( + const WrappedElement = graphql(query)( ({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
), ); @@ -358,8 +369,10 @@ describe('SSR', () => { const WrappedElement = () => ( - {({ data: { currentUser }, loading }) => ( -
{loading ? 'loading' : currentUser.firstName}
+ {({ data, loading }) => ( +
+ {loading || !data ? 'loading' : data.currentUser.firstName} +
)}
); @@ -400,10 +413,14 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query, { + const WrappedElement = graphql(query, { options: { fetchPolicy: 'network-only' }, })(({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
)); const app = ( @@ -442,10 +459,14 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query, { + const WrappedElement = graphql(query, { options: { fetchPolicy: 'cache-and-network' }, })(({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
)); const app = ( @@ -485,9 +506,13 @@ describe('SSR', () => { }; } - const WrappedElement = graphql(query)( + const WrappedElement = graphql(query)( ({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
), ); @@ -513,7 +538,7 @@ describe('SSR', () => { }); it('should handle nested queries that depend on each other', () => { - const idQuery = gql` + const idQuery: DocumentNode = gql` { currentUser { id @@ -521,7 +546,7 @@ describe('SSR', () => { } `; const idData = { currentUser: { id: '1234' } }; - const userQuery = gql` + const userQuery: DocumentNode = gql` query getUser($id: String) { user(id: $id) { firstName @@ -544,22 +569,50 @@ describe('SSR', () => { }); interface Props {} - interface Data { + interface IdQueryData { currentUser: { id: string; }; } - const withId = graphql(idQuery); - const withUser = graphql(userQuery, { + interface UserQueryData { + user: { + firstName: string; + }; + } + + interface UserQueryVariables { + id: string; + } + + type WithIdChildProps = ChildProps; + const withId = graphql(idQuery); + + type WithUserChildProps = ChildProps< + Props, + UserQueryData, + UserQueryVariables + >; + const withUser = graphql< + WithIdChildProps, + UserQueryData, + UserQueryVariables + >(userQuery, { skip: ({ data: { loading } }) => loading, - options: ({ data }: ChildProps) => ({ - variables: { id: data.currentUser.id }, + options: ({ data }) => ({ + variables: { id: data!.currentUser!.id }, }), }); - const Component = ({ data }) => ( -
{data.loading ? 'loading' : data.user.firstName}
+ const Component: React.StatelessComponent = ({ + data, + }) => ( +
+ {!data || data.loading || !data.user + ? 'loading' + : data.user.firstName} +
); + const WrappedComponent = withId(withUser(Component)); const app = ( @@ -598,9 +651,9 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query)( + const WrappedElement = graphql(query)( ({ data }: ChildProps) => ( -
{data.loading ? 'loading' : data.error}
+
{!data || data.loading ? 'loading' : data.error}
), ); @@ -655,7 +708,7 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query, { + const WrappedElement = graphql(query, { skip: true, })(({ data }: ChildProps) => (
{!data ? 'skipped' : 'dang'}
@@ -681,11 +734,11 @@ describe('SSR', () => { } } `; - const data = { currentUser: { firstName: 'James' } }; - const variables = { id: 1 }; + const resultData = { currentUser: { firstName: 'James' } }; + const variables = { id: '1' }; const link = mockSingleLink({ request: { query, variables }, - result: { data }, + result: { data: resultData }, delay: 50, }); const cache = new Cache({ addTypename: false }); @@ -694,28 +747,39 @@ describe('SSR', () => { cache, }); - interface Props {} + interface Props { + id: string; + } interface Data { currentUser: { firstName: string; }; } - const Element = graphql(query, { - name: 'user', - })(({ user }: ChildProps & { user: DataValue }) => ( -
{user.loading ? 'loading' : user.currentUser.firstName}
- )); + interface Variables { + id: string; + } + const Element = graphql(query)( + ({ data }: ChildProps) => ( +
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
+ ), + ); const app = ( - + ); return getDataFromTree(app).then(() => { const initialState = cache.extract(); expect(initialState).toBeTruthy(); - expect(initialState['$ROOT_QUERY.currentUser({"id":1})']).toBeTruthy(); + expect( + initialState['$ROOT_QUERY.currentUser({"id":"1"})'], + ).toBeTruthy(); }); }); @@ -727,11 +791,11 @@ describe('SSR', () => { } } `; - const data = { currentUser: { firstName: 'James' } }; - const variables = { id: 1 }; + const resultData = { currentUser: { firstName: 'James' } }; + const variables = { id: '1' }; const link = mockSingleLink({ request: { query, variables }, - result: { data }, + result: { data: resultData }, delay: 50, }); @@ -741,8 +805,22 @@ describe('SSR', () => { cache, }); - @graphql(query, { name: 'user' }) - class Element extends React.Component { + interface Props { + id: string; + } + interface Data { + currentUser: { + firstName: string; + }; + } + interface Variables { + id: string; + } + + class Element extends React.Component< + ChildProps, + { thing: number } + > { state = { thing: 1 }; componentWillMount() { @@ -750,17 +828,23 @@ describe('SSR', () => { } render() { - const { user } = this.props; + const { data } = this.props; expect(this.state.thing).toBe(2); return ( -
{user.loading ? 'loading' : user.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
); } } + const ElementWithData = graphql(query)(Element); + const app = ( - + ); @@ -769,7 +853,7 @@ describe('SSR', () => { const initialState = cache.extract(); expect(initialState).toBeTruthy(); expect( - initialState['$ROOT_QUERY.currentUser({"id":1})'], + initialState['$ROOT_QUERY.currentUser({"id":"1"})'], ).toBeTruthy(); done(); }) @@ -795,9 +879,8 @@ describe('SSR', () => { }); it('should maintain any state set in the element constructor', () => { - class Element extends React.Component { - s; - constructor(props) { + class Element extends React.Component<{}, { foo: string }> { + constructor(props: {}) { super(props); this.state = { foo: 'bar' }; } @@ -819,11 +902,11 @@ describe('SSR', () => { } } `; - const data = { currentUser: { firstName: 'James' } }; - const variables = { id: 1 }; + const resultData = { currentUser: { firstName: 'James' } }; + const variables = { id: '1' }; const link = mockSingleLink({ request: { query, variables }, - result: { data }, + result: { data: resultData }, delay: 50, }); const apolloClient = new ApolloClient({ @@ -833,22 +916,25 @@ describe('SSR', () => { }), }); interface Props { - id: number; + id: string; } interface Data { currentUser: { firstName: string; }; } + interface Variables { + id: string; + } + interface State { thing: number; - userId: number; - client: null | any; + userId: null | number; + client: null | ApolloClient; } - @graphql(query, { name: 'user' }) class Element extends React.Component< - ChildProps & { user?: DataValue }, + ChildProps, State > { state: State = { @@ -859,7 +945,11 @@ describe('SSR', () => { componentWillMount() { this.setState( - (state, props, context) => + ( + state: State, + props: Props, + context: { client: ApolloClient }, + ) => ({ thing: state.thing + 1, userId: props.id, @@ -869,19 +959,25 @@ describe('SSR', () => { } render() { - const { user, id } = this.props; + const { data, id } = this.props; expect(this.state.thing).toBe(2); expect(this.state.userId).toBe(id); expect(this.state.client).toBe(apolloClient); return ( -
{user.loading ? 'loading' : user.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
); } } + const ElementWithData = graphql(query)(Element); + const app = ( - + ); @@ -890,7 +986,7 @@ describe('SSR', () => { const initialState = apolloClient.cache.extract(); expect(initialState).toBeTruthy(); expect( - initialState['$ROOT_QUERY.currentUser({"id":1})'], + initialState['$ROOT_QUERY.currentUser({"id":"1"})'], ).toBeTruthy(); done(); }) @@ -905,11 +1001,11 @@ describe('SSR', () => { } } `; - const data = { currentUser: { firstName: 'James' } }; - const variables = { id: 1 }; + const resultData = { currentUser: { firstName: 'James' } }; + const variables = { id: '1' }; const link = mockSingleLink({ request: { query, variables }, - result: { data }, + result: { data: resultData }, delay: 50, }); @@ -925,16 +1021,31 @@ describe('SSR', () => { }; } - const Element = graphql(query, { - name: 'user', + interface Props { + id: string; + } + interface Data { + currentUser: { + firstName: string; + }; + } + interface Variables { + id: string; + } + + const Element = graphql(query, { options: props => ({ variables: props, ssr: false }), - })(({ user }: { user?: DataValue }) => ( -
{user.loading ? 'loading' : user.currentUser.firstName}
+ })(({ data }) => ( +
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
)); const app = ( - + ); @@ -954,7 +1065,7 @@ describe('SSR', () => { } `; const resultData = { currentUser: { firstName: 'James' } }; - const variables = { id: 1 }; + const variables = { id: '1' }; const link = mockSingleLink({ request: { query, variables }, result: { data: resultData }, @@ -973,19 +1084,21 @@ describe('SSR', () => { }; } - class CurrentUserQuery extends Query {} + class CurrentUserQuery extends Query {} - const Element = (props: { id: number }) => ( + const Element = (props: { id: string }) => ( {({ data, loading }) => ( -
{loading || !data ? 'loading' : data.currentUser.firstName}
+
+ {loading || !data ? 'loading' : data.currentUser.firstName} +
)}
); const app = ( - + ); @@ -1028,36 +1141,52 @@ describe('SSR', () => { cache: new Cache({ addTypename: false }), }); - interface Data {} + interface Data { + currentUser: { + firstName: string; + }; + } interface QueryProps {} - const withQuery = graphql(query, { - options: ownProps => ({ ssr: true }), + interface QueryChildProps { + refetchQuery: Function; + data: DataValue; + } + + const withQuery = graphql(query, { + options: () => ({ ssr: true }), props: ({ data }) => { - expect(data.refetch).toBeTruthy(); + expect(data!.refetch).toBeTruthy(); return { - refetchQuery: data.refetch, - data, + refetchQuery: data!.refetch, + data: data!, }; }, }); - interface MutationProps { - refetchQuery: Function; - data: Data; - } - const withMutation = graphql(mutation, { + const withMutation = graphql< + QueryChildProps, + {}, + {}, + { action: (variables: {}) => Promise } + >(mutation, { props: ({ ownProps, mutate }) => { expect(ownProps.refetchQuery).toBeTruthy(); return { - action(variables) { - return mutate({ variables }).then(() => ownProps.refetchQuery()); + action(variables: {}) { + return mutate!({ variables }).then(() => ownProps.refetchQuery()); }, }; }, }); - const Element = ({ data }) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+ const Element: React.StatelessComponent< + QueryChildProps & { action: (variables: {}) => Promise } + > = ({ data }) => ( +
+ {data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
); const WrappedElement = withQuery(withMutation(Element)); @@ -1133,8 +1262,14 @@ describe('SSR', () => { }, }); - const Element = ({ data }) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+ const Element: React.StatelessComponent< + ChildProps, QueryData, {}> + > = ({ data }) => ( +
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
); const WrappedElement = withMutation(withQuery(Element)); @@ -1175,14 +1310,18 @@ describe('SSR', () => { cache: new Cache({ addTypename: false }), }); - const WrappedElement = graphql(query)( + const WrappedElement = graphql<{}, Data>(query)( ({ data }: ChildProps<{}, Data>) => ( -
{data.loading ? 'loading' : data.currentUser.firstName}
+
+ {!data || data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
), ); - class MyRootContainer extends React.Component { - constructor(props) { + class MyRootContainer extends React.Component<{}, { color: string }> { + constructor(props: {}) { super(props); this.state = { color: 'purple' }; } diff --git a/test/server/server.test.tsx b/test/server/server.test.tsx index 9d306cf65a..f5a5c8b5a4 100644 --- a/test/server/server.test.tsx +++ b/test/server/server.test.tsx @@ -8,8 +8,14 @@ import { GraphQLList, GraphQLString, GraphQLID, + DocumentNode, } from 'graphql'; -import { graphql, ApolloProvider, renderToStringWithData } from '../../src'; +import { + graphql, + ApolloProvider, + renderToStringWithData, + ChildProps, +} from '../../src'; import gql from 'graphql-tag'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; @@ -93,7 +99,7 @@ describe('SSR', () => { name: { type: GraphQLString }, films: { type: new GraphQLList(FilmType), - resolve: ({ films }) => films.map(id => filmMap.get(id)), + resolve: ({ films }) => films.map((id: string) => filmMap.get(id)), }, }, }); @@ -153,9 +159,9 @@ describe('SSR', () => { title } } - `) + ` as DocumentNode) class Film extends React.Component { - render() { + render(): React.ReactNode { const { data } = this.props; if (data.loading) return null; const { film } = data; @@ -163,7 +169,18 @@ describe('SSR', () => { } } - @graphql(gql` + interface ShipData { + ship: { + name: string; + films: { id: string }[]; + }; + } + + interface ShipVariables { + id: string; + } + + @graphql(gql` query data($id: ID!) { ship(id: $id) { name @@ -172,15 +189,17 @@ describe('SSR', () => { } } } - `) - class Starship extends React.Component { - render() { + ` as DocumentNode) + class Starship extends React.Component< + ChildProps + > { + render(): React.ReactNode { const { data } = this.props; - if (data.loading) return null; + if (!data || data.loading || !data.ship) return null; const { ship } = data; return (
-

{ship.name} appeared in the following flims:

+

{ship.name} appeared in the following films:


    {ship.films.map((film, key) => ( @@ -194,21 +213,25 @@ describe('SSR', () => { } } - @graphql( - gql` - query data { - allShips { - id - } + interface AllShipsData { + allShips: { id: string }[]; + } + + @graphql<{}, AllShipsData>(gql` + query data { + allShips { + id } - `, - ) - class AllShips extends React.Component { - render() { + } + ` as DocumentNode) + class AllShips extends React.Component> { + render(): React.ReactNode { const { data } = this.props; return (
      - {!data.loading && + {data && + !data.loading && + data.allShips && data.allShips.map((ship, key) => (
    • @@ -219,23 +242,25 @@ describe('SSR', () => { } } - @graphql( - gql` - query data { - allPlanets { - name - } + interface AllPlanetsData { + allPlanets: { name: string }[]; + } + + @graphql<{}, AllPlanetsData>(gql` + query data { + allPlanets { + name } - `, - ) - class AllPlanets extends React.Component { - render() { + } + ` as DocumentNode) + class AllPlanets extends React.Component> { + render(): React.ReactNode { const { data } = this.props; - if (data.loading) return null; + if (!data || data.loading) return null; return (

      Planets

      - {data.allPlanets.map((planet, key) => ( + {(data.allPlanets || []).map((planet, key) => (
      {planet.name}
      ))}
      diff --git a/test/test-utils.test.tsx b/test/test-utils.test.tsx index d94c2aad64..e676f56bed 100644 --- a/test/test-utils.test.tsx +++ b/test/test-utils.test.tsx @@ -4,8 +4,9 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import gql from 'graphql-tag'; -import { graphql } from '../src'; +import { graphql, ChildProps } from '../src'; import { MockedProvider, mockSingleLink } from '../src/test-utils'; +import { DocumentNode } from 'graphql'; const variables = { username: 'mock_username', @@ -20,7 +21,7 @@ const user = { ...userWithoutTypeName, }; -const query = gql` +const query: DocumentNode = gql` query GetUser($username: String!) { user(username: $username) { id @@ -28,7 +29,7 @@ const query = gql` } } `; -const queryWithoutTypename = gql` +const queryWithoutTypename: DocumentNode = gql` query GetUser($username: String!) { user(username: $username) { id @@ -36,17 +37,31 @@ const queryWithoutTypename = gql` } `; -const withUser = graphql(queryWithoutTypename, { +interface Data { + user: { + id: string; + }; +} + +interface Variables { + username: string; +} + +const withUser = graphql(queryWithoutTypename, { options: props => ({ variables: props, }), }); it('mocks the data and adds the typename to the query', done => { - class Container extends React.Component { - componentWillReceiveProps(nextProps) { + class Container extends React.Component< + ChildProps + > { + componentWillReceiveProps( + nextProps: ChildProps, + ) { try { - expect(nextProps.data.user).toMatchSnapshot(); + expect(nextProps.data!.user).toMatchSnapshot(); done(); } catch (e) { done.fail(e); @@ -78,11 +93,15 @@ it('mocks the data and adds the typename to the query', done => { }); it('errors if the variables in the mock and component do not match', done => { - class Container extends React.Component { - componentWillReceiveProps(nextProps) { + class Container extends React.Component< + ChildProps + > { + componentWillReceiveProps( + nextProps: ChildProps, + ) { try { - expect(nextProps.data.user).toBeUndefined(); - expect(nextProps.data.error).toMatchSnapshot(); + expect(nextProps.data!.user).toBeUndefined(); + expect(nextProps.data!.error).toMatchSnapshot(); done(); } catch (e) { done.fail(e); @@ -118,10 +137,14 @@ it('errors if the variables in the mock and component do not match', done => { }); it('mocks a network error', done => { - class Container extends React.Component { - componentWillReceiveProps(nextProps) { + class Container extends React.Component< + ChildProps + > { + componentWillReceiveProps( + nextProps: ChildProps, + ) { try { - expect(nextProps.data.error).toEqual( + expect(nextProps.data!.error).toEqual( new Error('Network error: something went wrong'), ); done(); @@ -155,10 +178,14 @@ it('mocks a network error', done => { }); it('mocks the data without adding the typename', done => { - class Container extends React.Component { - componentWillReceiveProps(nextProps) { + class Container extends React.Component< + ChildProps + > { + componentWillReceiveProps( + nextProps: ChildProps, + ) { try { - expect(nextProps.data.user).toMatchSnapshot(); + expect(nextProps.data!.user).toMatchSnapshot(); done(); } catch (e) { done.fail(e); @@ -202,10 +229,14 @@ it('allows for passing a custom client', done => { cache: new InMemoryCache(), }); - class Container extends React.Component { - componentWillReceiveProps(nextProps) { + class Container extends React.Component< + ChildProps + > { + componentWillReceiveProps( + nextProps: ChildProps, + ) { try { - expect(nextProps.data.user).toMatchSnapshot(); + expect(nextProps.data!.user).toMatchSnapshot(); done(); } catch (e) { done.fail(e); diff --git a/test/test-utils/catchAsyncError.ts b/test/test-utils/catchAsyncError.ts index fe4a618332..e907f43195 100644 --- a/test/test-utils/catchAsyncError.ts +++ b/test/test-utils/catchAsyncError.ts @@ -1,4 +1,4 @@ -const catchAsyncError = (done, cb) => { +const catchAsyncError = (done: jest.DoneCallback, cb: () => void) => { try { cb(); } catch (e) { diff --git a/test/test-utils/createClient.ts b/test/test-utils/createClient.ts index 6353bd59a0..6df68fc94e 100644 --- a/test/test-utils/createClient.ts +++ b/test/test-utils/createClient.ts @@ -2,13 +2,14 @@ import { mockSingleLink } from '../../src/test-utils'; import { InMemoryCache } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import { NormalizedCacheObject } from 'apollo-cache-inmemory/src/types'; +import { DocumentNode } from 'graphql'; /** * helper for most common test client creation usage */ -export default function createClient( - data, - query, +export default function createClient( + data: TData, + query: DocumentNode, variables = {}, ): ApolloClient { return new ApolloClient({ diff --git a/test/test-utils/enzyme.ts b/test/test-utils/enzyme.ts index af57b8ae0f..510077f33e 100644 --- a/test/test-utils/enzyme.ts +++ b/test/test-utils/enzyme.ts @@ -1,4 +1,4 @@ import * as Enzyme from 'enzyme'; -import * as Adapter from 'enzyme-adapter-react-16'; +import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }); diff --git a/test/test-utils/shim.ts b/test/test-utils/shim.ts index 711c93cfe8..3b2cd672b0 100644 --- a/test/test-utils/shim.ts +++ b/test/test-utils/shim.ts @@ -4,6 +4,9 @@ // Prevent typescript from giving error. const globalAny: any = global; -globalAny.requestAnimationFrame = callback => { +const raf: typeof requestAnimationFrame = callback => { setTimeout(callback, 0); + return 0; }; + +globalAny.requestAnimationFrame = raf; diff --git a/test/test-utils/stripSymbols.ts b/test/test-utils/stripSymbols.ts index 233ed4deb2..22ac117c41 100644 --- a/test/test-utils/stripSymbols.ts +++ b/test/test-utils/stripSymbols.ts @@ -2,6 +2,6 @@ * Apollo-client adds Symbols to the data in the store. In order to make * assertions in our tests easier we strip these Symbols from the data. */ -export default function stripSymbols(data) { +export default function stripSymbols(data: T): T { return JSON.parse(JSON.stringify(data)); } diff --git a/test/test-utils/wait.ts b/test/test-utils/wait.ts index f818b7b03f..32efdf4c08 100644 --- a/test/test-utils/wait.ts +++ b/test/test-utils/wait.ts @@ -1,3 +1,3 @@ -export default function wait(ms) { +export default function wait(ms: number): Promise { return new Promise(resolve => setTimeout(() => resolve(), ms)); } diff --git a/test/test-utils/wrap.ts b/test/test-utils/wrap.ts index bcda107a7a..fe7c9abe17 100644 --- a/test/test-utils/wrap.ts +++ b/test/test-utils/wrap.ts @@ -1,9 +1,10 @@ // XXX: this is also defined in apollo-client // I'm not sure why mocha doesn't provide something like this, you can't // always use promises -const wrap = (done: Function, cb: (...args: any[]) => any) => ( - ...args: any[] -) => { +const wrap = ( + done: jest.DoneCallback, + cb: (...args: TArgs[]) => void, +) => (...args: TArgs[]) => { try { return cb(...args); } catch (e) { diff --git a/test/typescript-usage.tsx b/test/typescript-usage.tsx index c109ddbf0f..14bac2e307 100644 --- a/test/typescript-usage.tsx +++ b/test/typescript-usage.tsx @@ -4,8 +4,8 @@ // that the are handled import * as React from 'react'; import gql from 'graphql-tag'; -import { graphql } from '../src'; -import { ChildProps, NamedProps, GraphqlQueryControls } from '../src'; +import { graphql, DataValue } from '../src'; +import { ChildProps } from '../src'; const historyQuery = gql` query history($solutionId: String) { @@ -65,7 +65,11 @@ const withHistory = graphql(historyQuery, { class HistoryView extends React.Component> { render() { - if (this.props.data.history.length > 0) { + if ( + this.props.data && + this.props.data.history && + this.props.data.history.length > 0 + ) { return
      yay type checking works
      ; } else { return null; @@ -85,7 +89,7 @@ const HistoryViewSFC = graphql(historyQuery, { }, }), })(props => { - if (this.props.data.history.length > 0) { + if (props.data && props.data.history && props.data.history.length > 0) { return
      yay type checking works
      ; } else { return null; @@ -98,29 +102,55 @@ const HistoryViewSFC = graphql(historyQuery, { // decorator @graphql(historyQuery) class DecoratedHistoryView extends React.Component> { - render() { - if (this.props.data.history.length > 0) { + render(): React.ReactNode { + if ( + this.props.data && + this.props.data.history && + this.props.data.history.length > 0 + ) { return
      yay type checking works
      ; } else { return null; } } } + ; // tslint:disable-line // -------------------------- -// with using name -const withHistoryUsingName = graphql(historyQuery, { - name: 'organisationData', - props: ({ - organisationData, - }: NamedProps<{ organisationData: GraphqlQueryControls & Data }, Props>) => ({ - ...organisationData, +// with custom props +const withProps = graphql< + Props, + Data, + {}, + { organisationData: DataValue | undefined } +>(historyQuery, { + props: ({ data }) => ({ + organisationData: data, }), }); -const HistoryViewUsingName = withHistoryUsingName(HistoryView); -; // tslint:disable-line +const Foo = withProps(props => ( +
      Woot {props.organisationData!.history}
      +)); + +; // tslint:disable-line + +// -------------------------- +// It is not recommended to use `name` with Typescript, better to use props and map the property +// explicitly so it can be type checked. +// with using name +// const withHistoryUsingName = graphql(historyQuery, { +// name: 'organisationData', +// props: ({ +// organisationData, +// }: NamedProps<{ organisationData: GraphqlQueryControls & Data }, Props>) => ({ +// ...organisationData, +// }), +// }); + +// const HistoryViewUsingName = withHistoryUsingName(HistoryView); +// ; // tslint:disable-line // -------------------------- // mutation with name diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index c560ed6ed5..ab8307deae 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -5,9 +5,10 @@ "lib": ["es2015", "dom"], "moduleResolution": "node", "sourceMap": true, - "noImplicitAny": false, + // "noImplicitAny": false, + "strict": true, "outDir": "lib", - "allowSyntheticDefaultImports": false, + "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "pretty": true, "removeComments": true, diff --git a/tsconfig.json b/tsconfig.json index 7cfde95e2b..1fb51572a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,15 +4,17 @@ "target": "es2015", "lib": ["es2015", "dom"], "moduleResolution": "node", - "noImplicitAny": false, + // "noImplicitAny": false, + "strict": true, "outDir": "lib", - "allowSyntheticDefaultImports": false, + "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "pretty": true, "removeComments": true, "jsx": "react", "skipLibCheck": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["./typings/**/*", "./src/**/*", "./test/**/*"], "exclude": ["./node_modules", "./dist", "./lib"] diff --git a/typings/fbjs_lib_shallow-equal.d.ts b/typings/fbjs_lib_shallow-equal.d.ts new file mode 100644 index 0000000000..6a711e0cd0 --- /dev/null +++ b/typings/fbjs_lib_shallow-equal.d.ts @@ -0,0 +1,5 @@ +declare module 'fbjs/lib/shallowEqual' { + function shallowEqual(a: any, b: any): boolean; + + export default shallowEqual; +} diff --git a/typings/hoist-non-react-statics.d.ts b/typings/hoist-non-react-statics.d.ts index f4059e74c6..6be96c56e5 100644 --- a/typings/hoist-non-react-statics.d.ts +++ b/typings/hoist-non-react-statics.d.ts @@ -5,11 +5,14 @@ declare module 'hoist-non-react-statics' { * * Returns the target component. */ - function hoistNonReactStatics( - targetComponent: any, - sourceComponent: any, + function hoistNonReactStatics< + TTargetComponent extends React.ComponentType, + TSourceComponent extends React.ComponentType + >( + targetComponent: TTargetComponent, + sourceComponent: TSourceComponent, customStatics: { [name: string]: boolean }, - ): any; + ): TTargetComponent; namespace hoistNonReactStatics { } diff --git a/yarn.lock b/yarn.lock index 663c987319..6702d5f7be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,7 +36,13 @@ version "0.22.6" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.6.tgz#ad8c630a942efe3fc59165857851b55f95de2d50" -"@types/enzyme@3.1.8": +"@types/enzyme-adapter-react-16@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.1.tgz#cb93338ccf6cd1b8fdda91027c4ffe56583537b5" + dependencies: + "@types/enzyme" "*" + +"@types/enzyme@*", "@types/enzyme@3.1.8": version "3.1.8" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.8.tgz#e0a4492994fafb2fccc1726f8b4d9960097a4a8c" dependencies: @@ -67,6 +73,10 @@ version "4.0.30" resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652" +"@types/prop-types@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1" + "@types/react-dom@16.0.3": version "16.0.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" @@ -74,13 +84,21 @@ "@types/node" "*" "@types/react" "*" -"@types/react@*": - version "16.0.31" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.31.tgz#5285da62f3ac62b797f6d0729a1d6181f3180c3e" +"@types/react-test-renderer@16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.0.0.tgz#905a12076d7315eb4a36c4f0e5e760c7b3115420" + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@16.0.35": + version "16.0.35" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.35.tgz#7ce8a83cad9690fd965551fc513217a74fc9e079" -"@types/react@16.0.34": - version "16.0.34" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.34.tgz#7a8f795afd8a404a9c4af9539b24c75d3996914e" +"@types/recompose@0.24.4": + version "0.24.4" + resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.24.4.tgz#39de3048382d39f0808a1780d3aa2fdb839986cd" + dependencies: + "@types/react" "*" "@types/zen-observable@0.5.3", "@types/zen-observable@^0.5.3": version "0.5.3"