From f1f233380f34e75c521950ce2d7b5c8ac470c353 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Thu, 25 Jan 2018 09:07:43 -0300 Subject: [PATCH 01/14] Stricter type check in getDataFromTree --- package.json | 1 + src/getDataFromTree.ts | 196 +++++++++++++++++++++++++---------------- tsconfig.json | 6 +- yarn.lock | 45 +++++++++- 4 files changed, 167 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index a6149b442d..9fa885e377 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/jest": "22.0.1", "@types/lodash": "4.14.92", "@types/object-assign": "4.0.30", + "@types/prop-types": "^15.5.2", "@types/react": "16.0.34", "@types/react-dom": "16.0.3", "@types/zen-observable": "0.5.3", diff --git a/src/getDataFromTree.ts b/src/getDataFromTree.ts index c9ce72cb4a..d83488e5fc 100755 --- a/src/getDataFromTree.ts +++ b/src/getDataFromTree.ts @@ -1,5 +1,13 @@ -import { Children, ReactElement, StatelessComponent } from 'react'; -import ApolloClient, { ApolloQueryResult } from 'apollo-client'; +import { + Children, + ReactElement, + ReactNode, + Component, + ComponentType, + ComponentClass, + ChildContextProvider +} from 'react'; +import ApolloClient from 'apollo-client'; const assign = require('object-assign'); export interface Context { @@ -14,31 +22,47 @@ 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; +} + +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 +74,113 @@ 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) { - // 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 +204,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/tsconfig.json b/tsconfig.json index 7cfde95e2b..c9e010b09b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "target": "es2015", "lib": ["es2015", "dom"], "moduleResolution": "node", - "noImplicitAny": false, + // "noImplicitAny": false, + "strict": true, "outDir": "lib", "allowSyntheticDefaultImports": false, "experimentalDecorators": true, @@ -12,7 +13,8 @@ "removeComments": true, "jsx": "react", "skipLibCheck": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["./typings/**/*", "./src/**/*", "./test/**/*"], "exclude": ["./node_modules", "./dist", "./lib"] diff --git a/yarn.lock b/yarn.lock index 377dbe2bab..6438352f0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,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" @@ -90,6 +94,10 @@ acorn@^5.0.0, acorn@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" +acorn@^5.2.1: + version "5.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" + agent-base@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.1.2.tgz#80fa6cde440f4dcf9af2617cf246099b5d99f0c8" @@ -1710,6 +1718,14 @@ estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" +estree-walker@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa" + +estree-walker@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854" + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -3341,6 +3357,12 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +magic-string@^0.22.4: + version "0.22.4" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.4.tgz#31039b4e40366395618c1d6cf8193c53917475ff" + dependencies: + vlq "^0.2.1" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -4296,7 +4318,7 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.7, resolve@^1.3.2: +resolve@^1.1.7, resolve@^1.3.2, resolve@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" dependencies: @@ -4344,6 +4366,23 @@ rollup-plugin-babel-minify@^3.1.2: babel-core "^6.21.0" babel-preset-minify "^0.2.0" +rollup-plugin-commonjs@^8.2.6: + version "8.3.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.3.0.tgz#91b4ba18f340951e39ed7b1901f377a80ab3f9c3" + dependencies: + acorn "^5.2.1" + estree-walker "^0.5.0" + magic-string "^0.22.4" + resolve "^1.4.0" + rollup-pluginutils "^2.0.1" + +rollup-pluginutils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz#7ec95b3573f6543a46a6461bd9a7c544525d0fc0" + dependencies: + estree-walker "^0.3.0" + micromatch "^2.3.11" + rollup@^0.55.0: version "0.55.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.55.0.tgz#e547149333bdd91457a9e8101cf86783e21c576e" @@ -4861,6 +4900,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vlq@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" + vm2@patriksimek/vm2#custom_files: version "3.5.0" resolved "https://codeload.github.com/patriksimek/vm2/tar.gz/7e82f90ac705fc44fad044147cb0df09b4c79a57" From a7fdff80c3a4741f3c2bc10af9dc090ab76bc06b Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Sun, 28 Jan 2018 11:46:23 -0300 Subject: [PATCH 02/14] Stricter type check in Query --- package.json | 50 ++++++++++++++++++----------- src/ApolloConsumer.tsx | 2 +- src/ApolloProvider.tsx | 2 +- src/Query.tsx | 40 +++++++++++++++-------- src/getDataFromTree.ts | 2 +- src/parser.ts | 11 +++++-- test/test-utils/enzyme.ts | 2 +- tsconfig.json | 2 +- typings/fbjs_lib_shallow-equal.d.ts | 5 +++ yarn.lock | 17 ++++++++-- 10 files changed, 91 insertions(+), 42 deletions(-) create mode 100644 typings/fbjs_lib_shallow-equal.d.ts diff --git a/package.json b/package.json index 9fa885e377..b64239e7ca 100644 --- a/package.json +++ b/package.json @@ -19,19 +19,14 @@ "type-check": "tsc --project tsconfig.json --noEmit && flow check", "precompile": "rimraf lib", "compile": "npm run compile:esm && npm run compile:cjs", - "compile:esm": - "tsc --project tsconfig.json -d && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src && cd lib && rename js mjs", - "compile:cjs": - "tsc --project tsconfig.cjs.json && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src", - "postcompile": - "rollup -c rollup.config.js && rollup -c rollup.browser.config.js && ./scripts/prepare-package.sh", + "compile:esm": "tsc --project tsconfig.json -d && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src && cd lib && rename js mjs", + "compile:cjs": "tsc --project tsconfig.cjs.json && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src", + "postcompile": "rollup -c rollup.config.js && rollup -c rollup.browser.config.js && ./scripts/prepare-package.sh", "watch": "tsc -w", "lint": "tslint --project tsconfig.json --config tslint.json", - "lint:fix": - "npm run prettier && tslint 'src/*.ts*' --project tsconfig.json --fix", + "lint:fix": "npm run prettier && tslint 'src/*.ts*' --project tsconfig.json --fix", "lint-staged": "lint-staged", - "prettier": - "prettier --write \"{,!(node_modules|lib|coverage|npm)/**/}*.{ts*,js*,json,md}\"" + "prettier": "prettier --write \"{,!(node_modules|lib|coverage|npm)/**/}*.{ts*,js*,json,md}\"" }, "bundlesize": [ { @@ -40,8 +35,15 @@ } ], "lint-staged": { - "*.{ts*}": ["prettier --write", "npm run lint", "git add"], - "*.{js*,json,md}": ["prettier --write", "git add"] + "*.{ts*}": [ + "prettier --write", + "npm run lint", + "git add" + ], + "*.{js*,json,md}": [ + "prettier --write", + "git add" + ] }, "repository": { "type": "git", @@ -58,7 +60,9 @@ ], "author": "James Baxley ", "babel": { - "presets": ["env"] + "presets": [ + "env" + ] }, "jest": { "testEnvironment": "jsdom", @@ -67,15 +71,24 @@ "^.+\\.jsx?$": "babel-jest" }, "mapCoverage": true, - "moduleFileExtensions": ["ts", "tsx", "js", "json"], + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ], "modulePathIgnorePatterns": [ "/examples", "/test/flow-usage.js", "/test/typescript-usage.tsx" ], - "projects": [""], + "projects": [ + "" + ], "testRegex": "(/test/(?!test-utils\b)\b.*|\\.(test|spec))\\.(ts|tsx|js)$", - "setupFiles": ["/test/test-utils/setup.ts"] + "setupFiles": [ + "/test/test-utils/setup.ts" + ] }, "license": "MIT", "peerDependencies": { @@ -84,11 +97,12 @@ }, "devDependencies": { "@types/enzyme": "3.1.6", + "@types/enzyme-adapter-react-16": "^1.0.1", "@types/graphql": "0.11.7", - "@types/invariant": "2.2.29", + "@types/invariant": "^2.2.29", "@types/jest": "22.0.1", "@types/lodash": "4.14.92", - "@types/object-assign": "4.0.30", + "@types/object-assign": "^4.0.30", "@types/prop-types": "^15.5.2", "@types/react": "16.0.34", "@types/react-dom": "16.0.3", diff --git a/src/ApolloConsumer.tsx b/src/ApolloConsumer.tsx index 3b3e3f66c8..25b8856fef 100644 --- a/src/ApolloConsumer.tsx +++ b/src/ApolloConsumer.tsx @@ -1,7 +1,7 @@ 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; diff --git a/src/ApolloProvider.tsx b/src/ApolloProvider.tsx index 431a44128e..eaaaaa0f49 100644 --- a/src/ApolloProvider.tsx +++ b/src/ApolloProvider.tsx @@ -3,8 +3,8 @@ import * as PropTypes from 'prop-types'; import { Component } from 'react'; import ApolloClient from 'apollo-client'; import QueryRecyclerProvider from './QueryRecyclerProvider'; +import invariant from 'invariant'; -const invariant = require('invariant'); export interface ApolloProviderProps { client: ApolloClient; diff --git a/src/Query.tsx b/src/Query.tsx index 4b135f5281..5c360b3b19 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -5,7 +5,6 @@ import ApolloClient, { ApolloQueryResult, ApolloError, FetchMoreOptions, - UpdateQueryOptions, FetchMoreQueryOptions, FetchPolicy, ApolloCurrentResult, @@ -14,14 +13,18 @@ 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'; -const shallowEqual = require('fbjs/lib/shallowEqual'); const invariant = require('invariant'); -const pick = require('lodash/pick'); -type ObservableQueryFields = Pick, 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling'>; +type ObservableQueryFields = Pick, 'refetch' | 'fetchMore' | 'startPolling' | 'stopPolling'> & { + updateQuery: ( + mapFn: (previousQueryResult: TData, options: { variables?: TVariables }) => TData, + ) => void; +}; -function observableQueryFields(observable: ObservableQuery): ObservableQueryFields { +function observableQueryFields(observable: ObservableQuery): ObservableQueryFields { const fields = pick( observable, 'refetch', @@ -31,13 +34,17 @@ function observableQueryFields(observable: ObservableQuery): Obser 'stopPolling', ); - Object.keys(fields).forEach(key => { - if (typeof fields[key] === 'function') { - fields[key] = fields[key].bind(observable); + Object.keys(fields).forEach((key) => { + 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 { @@ -57,7 +64,7 @@ export interface QueryResult { startPolling: (pollInterval: number) => void; stopPolling: () => void; updateQuery: ( - mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any, + mapFn: (previousQueryResult: TData, options: { variables?: TVariables }) => TData, ) => void; } @@ -75,6 +82,10 @@ export interface QueryState { result: ApolloCurrentResult; } +interface QueryContext { + client: ApolloClient; +} + class Query extends React.Component< QueryProps, QueryState @@ -82,13 +93,14 @@ class Query extends React.Componen 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 +142,7 @@ 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 +169,7 @@ class Query extends React.Componen return children(queryResult); } - private initializeQueryObservable = props => { + private initializeQueryObservable = (props: QueryProps) => { const { variables, pollInterval, diff --git a/src/getDataFromTree.ts b/src/getDataFromTree.ts index d83488e5fc..b622ec2f49 100755 --- a/src/getDataFromTree.ts +++ b/src/getDataFromTree.ts @@ -8,7 +8,7 @@ import { ChildContextProvider } from 'react'; import ApolloClient from 'apollo-client'; -const assign = require('object-assign'); +import assign from 'object-assign'; export interface Context { client?: ApolloClient; 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/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/tsconfig.json b/tsconfig.json index c9e010b09b..1fb51572a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ // "noImplicitAny": false, "strict": true, "outDir": "lib", - "allowSyntheticDefaultImports": false, + "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "pretty": true, "removeComments": true, 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/yarn.lock b/yarn.lock index 6438352f0a..9f40fd0e77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,6 +22,19 @@ version "0.22.6" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.6.tgz#ad8c630a942efe3fc59165857851b55f95de2d50" +"@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@*": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.8.tgz#e0a4492994fafb2fccc1726f8b4d9960097a4a8c" + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + "@types/enzyme@3.1.6": version "3.1.6" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.6.tgz#5b6fd8c5d23d2e1d06eca528b54df81c3ee4cbbf" @@ -33,7 +46,7 @@ version "0.11.7" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.11.7.tgz#da39a2f7c74e793e32e2bb7b3b68da1691532dd5" -"@types/invariant@2.2.29": +"@types/invariant@^2.2.29": version "2.2.29" resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.29.tgz#aa845204cd0a289f65d47e0de63a6a815e30cc66" @@ -49,7 +62,7 @@ version "8.5.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5" -"@types/object-assign@4.0.30": +"@types/object-assign@^4.0.30": version "4.0.30" resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652" From 47f77c7d4bb947e74123e1b7ec6dd6e160c174db Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Sun, 28 Jan 2018 18:28:02 -0300 Subject: [PATCH 03/14] Strict type check for withApollo and Subscription --- src/Subscriptions.tsx | 26 +++++++++++++++----------- src/withApollo.tsx | 32 ++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Subscriptions.tsx b/src/Subscriptions.tsx index 1944c67c54..d83c680c97 100644 --- a/src/Subscriptions.tsx +++ b/src/Subscriptions.tsx @@ -13,13 +13,13 @@ const invariant = require('invariant'); export interface SubscriptionResult { loading: boolean; data?: TData; - error: ApolloError; + error?: ApolloError; } -export interface SubscriptionProps { +export interface SubscriptionProps { query: DocumentNode; - variables?: OperationVariables; - children: (result: any) => React.ReactNode; + variables?: TVariables; + children: (result: SubscriptionResult) => React.ReactNode; } export interface SubscriptionState { @@ -28,8 +28,12 @@ export interface SubscriptionState { error?: ApolloError; } -class Subscription extends React.Component< - SubscriptionProps, +interface SubscriptionContext { + client: ApolloClient; +} + +class Subscription extends React.Component< + SubscriptionProps, SubscriptionState > { static contextTypes = { @@ -40,7 +44,7 @@ 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 +60,7 @@ 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 +92,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 +114,7 @@ class Subscription extends React.Component< }; }; - private updateCurrentData = result => { + private updateCurrentData = (result: SubscriptionResult) => { this.setState({ data: result.data, loading: false, @@ -118,7 +122,7 @@ class Subscription extends React.Component< }); }; - private updateError = error => { + private updateError = (error: any) => { this.setState({ error, loading: false, diff --git a/src/withApollo.tsx b/src/withApollo.tsx index b1d1de9bac..72ae2bc392 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -1,29 +1,32 @@ import * as React from 'react'; import { OperationOption } from './types'; import ApolloConsumer from './ApolloConsumer'; +import { ApolloClient } from 'apollo-client'; 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'; } +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: WithApolloClient) { super(props); this.setWrappedInstance = this.setWrappedInstance.bind(this); } @@ -38,22 +41,23 @@ export default function withApollo( return this.wrappedInstance; } - setWrappedInstance(ref) { + setWrappedInstance(ref: React.ComponentType>) { this.wrappedInstance = ref; } render() { return ( - {client => ( - { + const props = Object.assign({}, + this.props, + { + client, + ref: operationOptions.withRef ? this.setWrappedInstance : undefined } - /> - )} + ); + return ; + }} ); } From e4647495885feace4d41d5b5e19765179c03338f Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Sun, 28 Jan 2018 19:12:52 -0300 Subject: [PATCH 04/14] Improve fetchMore type in Query. Some strict type checks in tests --- src/Query.tsx | 42 ++++++++----- test/client/Query.test.tsx | 88 ++++++++++++++++++---------- test/server/getDataFromTree.test.tsx | 8 +-- 3 files changed, 88 insertions(+), 50 deletions(-) diff --git a/src/Query.tsx b/src/Query.tsx index 5c360b3b19..8613d3e6cb 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -4,10 +4,9 @@ import ApolloClient, { ObservableQuery, ApolloQueryResult, ApolloError, - FetchMoreOptions, - FetchMoreQueryOptions, FetchPolicy, ApolloCurrentResult, + NetworkStatus, } from 'apollo-client'; import { DocumentNode } from 'graphql'; import { ZenObservable } from 'zen-observable-ts'; @@ -15,10 +14,29 @@ import { OperationVariables } from './types'; import { parser, DocumentType } from './parser'; import pick from 'lodash/pick'; import shallowEqual from 'fbjs/lib/shallowEqual'; +import invariant from 'invariant'; + +// Improved FetchMoreOptions type, need to port them back to Apollo Client +interface FetchMoreOptions { + updateQuery: (previousQueryResult: TData, options: { + fetchMoreResult?: TData; + variables: TVariables; + }) => TData; +} -const invariant = require('invariant'); +// Improved FetchMoreQueryOptions type, need to port them back to Apollo Client +interface FetchMoreQueryOptions { + variables: Pick; +} -type ObservableQueryFields = Pick, 'refetch' | 'fetchMore' | 'startPolling' | 'stopPolling'> & { +// Improved ObservableQuery field types, need to port them back to Apollo Client +type ObservableQueryFields = Pick, 'startPolling' | 'stopPolling'> & { + refetch: (variables?: TVariables) => Promise>; + fetchMore: ( + (fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions) => Promise> + ) & ( + (fetchMoreOptions: { query: DocumentNode } & FetchMoreQueryOptions & FetchMoreOptions) => Promise> + ); updateQuery: ( mapFn: (previousQueryResult: TData, options: { variables?: TVariables }) => TData, ) => void; @@ -55,17 +73,13 @@ export interface QueryResult { 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: TData, options: { variables?: TVariables }) => TData, - ) => void; + networkStatus: NetworkStatus; + fetchMore: ObservableQueryFields['fetchMore']; + refetch: ObservableQueryFields['refetch']; + startPolling: ObservableQueryFields['startPolling']; + stopPolling: ObservableQueryFields['stopPolling']; + updateQuery: ObservableQueryFields['updateQuery']; } export interface QueryProps { diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index 3a50781310..13b64fb92d 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,14 @@ 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 +34,10 @@ const allPeopleMocks = [ }, ]; +class AllPeopleQuery extends Query {} + describe('Query component', () => { - let wrapper; + let wrapper: ReactWrapper | null; beforeEach(() => { jest.useRealTimers(); }); @@ -80,7 +90,7 @@ describe('Query component', () => { it('renders using the children prop', done => { const Component = () => ( - {result =>

} + {_ =>
} ); wrapper = mount( @@ -89,14 +99,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 +184,7 @@ describe('Query component', () => { }); it('refetch', done => { - const queryRefetch = gql` + const queryRefetch: DocumentNode = gql` query people($first: Int) { allPeople(first: $first) { people { @@ -212,8 +222,9 @@ describe('Query component', () => { expect.assertions(5); + const Component = () => ( - { return null; }} - + ); wrapper = mount( @@ -294,8 +305,9 @@ describe('Query component', () => { let count = 0; expect.assertions(2); + const Component = () => ( - + {result => { if (result.loading) { return null; @@ -304,14 +316,15 @@ 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 +348,7 @@ describe('Query component', () => { count++; return null; }} - + ); wrapper = mount( @@ -486,10 +499,11 @@ describe('Query component', () => { }, ]; - let isUpdated; + let isUpdated = false; expect.assertions(3); + const Component = () => ( - + {result => { if (result.loading) { return null; @@ -517,7 +531,7 @@ describe('Query component', () => { return null; }} - + ); wrapper = mount( @@ -536,7 +550,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 +779,7 @@ describe('Query component', () => { const { variables } = this.state; return ( - + {result => { if (result.loading) { return null; @@ -785,7 +799,7 @@ describe('Query component', () => { count++; return null; }} - + ); } } @@ -950,7 +964,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 +998,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,9 +1023,11 @@ describe('Query component', () => { let count = 0; const noop = () => null; + class AllPeopleQuery2 extends Query {} + function Container() { return ( - + {(result) => { try { switch (count++) { @@ -1020,6 +1036,10 @@ 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( @@ -1028,7 +1048,7 @@ describe('Query component', () => { setTimeout(() => { result .refetch() - .then((val) => { + .then(() => { done.fail('Expected error value on first refetch.'); }, noop); }, 0); @@ -1060,6 +1080,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,7 +1097,7 @@ describe('Query component', () => { } return null; }} - + ) } diff --git a/test/server/getDataFromTree.test.tsx b/test/server/getDataFromTree.test.tsx index d0790666d9..55f2b140d2 100644 --- a/test/server/getDataFromTree.test.tsx +++ b/test/server/getDataFromTree.test.tsx @@ -80,7 +80,7 @@ describe('SSR', () => { it('functional stateless components', () => { let elementCount = 0; - const MyComponent = ({ n }) => ( + const MyComponent = ({ n }: { n: number }) => (
{_.times(n, i => )}
); walkTree(, {}, element => { @@ -126,7 +126,7 @@ 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} @@ -344,8 +344,8 @@ describe('SSR', () => { const WrappedElement = () => ( - {({ data: { currentUser }, loading }) => ( -
{loading ? 'loading' : currentUser.firstName}
+ {({ data, loading }) => ( +
{loading || !data ? 'loading' : data.currentUser.firstName}
)}
); From 3a1fed3ff5029588818455ddea94cdf909ec62c2 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Mon, 29 Jan 2018 12:26:20 -0300 Subject: [PATCH 05/14] Improve type checking in graphql HoC --- package.json | 49 ++++++---------- src/Query.tsx | 1 - src/graphql.tsx | 85 ++++++++++++++++------------ src/types.ts | 8 +-- test/typescript-usage.tsx | 19 +++++-- tsconfig.cjs.json | 5 +- typings/hoist-non-react-statics.d.ts | 8 +-- yarn.lock | 12 ++-- 8 files changed, 97 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index b64239e7ca..2929c195ba 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,19 @@ "type-check": "tsc --project tsconfig.json --noEmit && flow check", "precompile": "rimraf lib", "compile": "npm run compile:esm && npm run compile:cjs", - "compile:esm": "tsc --project tsconfig.json -d && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src && cd lib && rename js mjs", - "compile:cjs": "tsc --project tsconfig.cjs.json && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src", - "postcompile": "rollup -c rollup.config.js && rollup -c rollup.browser.config.js && ./scripts/prepare-package.sh", + "compile:esm": + "tsc --project tsconfig.json -d && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src && cd lib && rename js mjs", + "compile:cjs": + "tsc --project tsconfig.cjs.json && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src", + "postcompile": + "rollup -c rollup.config.js && rollup -c rollup.browser.config.js && ./scripts/prepare-package.sh", "watch": "tsc -w", "lint": "tslint --project tsconfig.json --config tslint.json", - "lint:fix": "npm run prettier && tslint 'src/*.ts*' --project tsconfig.json --fix", + "lint:fix": + "npm run prettier && tslint 'src/*.ts*' --project tsconfig.json --fix", "lint-staged": "lint-staged", - "prettier": "prettier --write \"{,!(node_modules|lib|coverage|npm)/**/}*.{ts*,js*,json,md}\"" + "prettier": + "prettier --write \"{,!(node_modules|lib|coverage|npm)/**/}*.{ts*,js*,json,md}\"" }, "bundlesize": [ { @@ -35,15 +40,8 @@ } ], "lint-staged": { - "*.{ts*}": [ - "prettier --write", - "npm run lint", - "git add" - ], - "*.{js*,json,md}": [ - "prettier --write", - "git add" - ] + "*.{ts*}": ["prettier --write", "npm run lint", "git add"], + "*.{js*,json,md}": ["prettier --write", "git add"] }, "repository": { "type": "git", @@ -60,9 +58,7 @@ ], "author": "James Baxley ", "babel": { - "presets": [ - "env" - ] + "presets": ["env"] }, "jest": { "testEnvironment": "jsdom", @@ -71,24 +67,15 @@ "^.+\\.jsx?$": "babel-jest" }, "mapCoverage": true, - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json" - ], + "moduleFileExtensions": ["ts", "tsx", "js", "json"], "modulePathIgnorePatterns": [ "/examples", "/test/flow-usage.js", "/test/typescript-usage.tsx" ], - "projects": [ - "" - ], + "projects": [""], "testRegex": "(/test/(?!test-utils\b)\b.*|\\.(test|spec))\\.(ts|tsx|js)$", - "setupFiles": [ - "/test/test-utils/setup.ts" - ] + "setupFiles": ["/test/test-utils/setup.ts"] }, "license": "MIT", "peerDependencies": { @@ -104,8 +91,8 @@ "@types/lodash": "4.14.92", "@types/object-assign": "^4.0.30", "@types/prop-types": "^15.5.2", - "@types/react": "16.0.34", - "@types/react-dom": "16.0.3", + "@types/react": "16.0.35", + "@types/react-dom": "^16.0.3", "@types/zen-observable": "0.5.3", "apollo-cache-inmemory": "1.1.5", "apollo-client": "2.2.0", diff --git a/src/Query.tsx b/src/Query.tsx index 8613d3e6cb..e1ee1d25ac 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -109,7 +109,6 @@ class Query extends React.Componen }; context: QueryContext; - state: QueryState; private client: ApolloClient; private queryObservable: ObservableQuery; private querySubscription: ZenObservable.Subscription; diff --git a/src/graphql.tsx b/src/graphql.tsx index f9b6483297..c771a08ac3 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -16,19 +16,23 @@ import { MutationFunc, OptionProps, } from './types'; +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 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'); const defaultMapPropsToOptions = () => ({}); -const defaultMapResultToProps = props => props; +const defaultMapResultToProps:

(props: P) => P = props => props; const defaultMapPropsToSkip = () => false; + +type ObservableQueryFields = Pick, '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 +45,16 @@ 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'; } @@ -60,7 +65,7 @@ export default function graphql< TProps = {}, TData = {}, TGraphQLVariables = {}, - TChildProps = ChildProps + TChildProps extends TProps = ChildProps >( document: DocumentNode, operationOptions: OperationOption = {}, @@ -87,13 +92,19 @@ export default function graphql< // Helps track hot reloading. const version = nextVersion++; - function wrapWithApolloComponent( - WrappedComponent: React.ComponentType, + function wrapWithApolloComponent( + WrappedComponent: React.ComponentType, ) { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; - type GraphqlProps = TOriginalProps & TGraphQLVariables; - class GraphQL extends React.Component { + type GraphqlProps = TProps & TGraphQLVariables; + + interface GraphqlContext { + client: ApolloClient; + getQueryRecycler: () => void; + } + + class GraphQL extends React.Component { static displayName = graphQLDisplayName; static WrappedComponent = WrappedComponent; static contextTypes = { @@ -102,7 +113,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 +129,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 +142,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 +174,7 @@ export default function graphql< } } - componentWillReceiveProps(nextProps, nextContext) { + componentWillReceiveProps(nextProps: Readonly, nextContext: GraphqlContext) { if (this.shouldSkip(nextProps)) { if (!this.shouldSkip(this.props)) { // if this has changed, we better unsubscribe @@ -251,7 +261,7 @@ export default function graphql< ); } - getClient(props): ApolloClient { + getClient(props: Readonly): ApolloClient { if (this.client) return this.client; const { client } = mapPropsToOptions(props); @@ -271,7 +281,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 +291,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, )}' ` + @@ -371,7 +384,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 +463,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,7 +528,7 @@ export default function graphql< return this.wrappedInstance; } - setWrappedInstance(ref) { + setWrappedInstance(ref: React.ComponentClass) { this.wrappedInstance = ref; } @@ -612,12 +625,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; @@ -637,9 +651,8 @@ export default function graphql< if (operationOptions.withRef) mergedPropsAndData.ref = this.setWrappedInstance; - this.renderedElement = React.createElement( - WrappedComponent, - mergedPropsAndData, + this.renderedElement = ( + ); return this.renderedElement; } diff --git a/src/types.ts b/src/types.ts index 6e23fd19c9..79eaa14bc0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -109,7 +109,7 @@ export interface OptionProps< > extends Partial>, Partial> { - ownProps: TProps; + ownProps: Readonly; } export interface OperationOption< @@ -121,15 +121,15 @@ export interface OperationOption< | QueryOpts | MutationOpts | (( - props: TProps, + props: Readonly, ) => QueryOpts | MutationOpts); props?: (props: OptionProps) => any; skip?: boolean | ((props: any) => boolean); name?: string; withRef?: boolean; shouldResubscribe?: ( - props: TProps & DataProps, - nextProps: TProps & DataProps, + props: Readonly, + nextProps: Readonly, ) => boolean; alias?: string; } diff --git a/test/typescript-usage.tsx b/test/typescript-usage.tsx index c109ddbf0f..86e9a1f9a0 100644 --- a/test/typescript-usage.tsx +++ b/test/typescript-usage.tsx @@ -65,7 +65,7 @@ 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 +85,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,16 +98,27 @@ 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 custom props +const withProps = graphql(historyQuery, { + props: ({ + data + }) => ({ + ...data, + }), +}); + // -------------------------- // with using name const withHistoryUsingName = graphql(historyQuery, { 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/typings/hoist-non-react-statics.d.ts b/typings/hoist-non-react-statics.d.ts index f4059e74c6..ef25efdd89 100644 --- a/typings/hoist-non-react-statics.d.ts +++ b/typings/hoist-non-react-statics.d.ts @@ -5,11 +5,11 @@ declare module 'hoist-non-react-statics' { * * Returns the target component. */ - function hoistNonReactStatics( - targetComponent: any, - sourceComponent: any, + function hoistNonReactStatics( + targetComponent: React.ComponentClass, + sourceComponent: React.ComponentType, customStatics: { [name: string]: boolean }, - ): any; + ): React.ComponentClass; namespace hoistNonReactStatics { } diff --git a/yarn.lock b/yarn.lock index 9f40fd0e77..2ceace01c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,20 +70,16 @@ 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": +"@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" dependencies: "@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@16.0.34": - version "16.0.34" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.34.tgz#7a8f795afd8a404a9c4af9539b24c75d3996914e" +"@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/zen-observable@0.5.3", "@types/zen-observable@^0.5.3": version "0.5.3" From de456442baebe390e5856c491b94650589500d6d Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Mon, 29 Jan 2018 13:10:01 -0300 Subject: [PATCH 06/14] Further improvements in graphql HoC. Prettier ran in some files --- src/ApolloProvider.tsx | 1 - src/Query.tsx | 63 +++++++++++++------ src/Subscriptions.tsx | 15 ++++- src/getDataFromTree.ts | 20 +++--- src/graphql.tsx | 27 +++++--- src/withApollo.tsx | 15 +++-- test/client/Query.test.tsx | 46 +++++++------- test/client/__snapshots__/Query.test.tsx.snap | 4 +- test/server/getDataFromTree.test.tsx | 16 ++++- test/typescript-usage.tsx | 16 +++-- 10 files changed, 144 insertions(+), 79 deletions(-) diff --git a/src/ApolloProvider.tsx b/src/ApolloProvider.tsx index eaaaaa0f49..49f97046ad 100644 --- a/src/ApolloProvider.tsx +++ b/src/ApolloProvider.tsx @@ -5,7 +5,6 @@ import ApolloClient from 'apollo-client'; import QueryRecyclerProvider from './QueryRecyclerProvider'; import invariant from 'invariant'; - export interface ApolloProviderProps { client: ApolloClient; children: React.ReactNode; diff --git a/src/Query.tsx b/src/Query.tsx index e1ee1d25ac..611d07f994 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -18,10 +18,13 @@ import invariant from 'invariant'; // Improved FetchMoreOptions type, need to port them back to Apollo Client interface FetchMoreOptions { - updateQuery: (previousQueryResult: TData, options: { + updateQuery: ( + previousQueryResult: TData, + options: { fetchMoreResult?: TData; variables: TVariables; - }) => TData; + }, + ) => TData; } // Improved FetchMoreQueryOptions type, need to port them back to Apollo Client @@ -30,19 +33,33 @@ interface FetchMoreQueryOptions { } // Improved ObservableQuery field types, need to port them back to Apollo Client -type ObservableQueryFields = Pick, 'startPolling' | 'stopPolling'> & { +type ObservableQueryFields = Pick< + ObservableQuery, + 'startPolling' | 'stopPolling' +> & { refetch: (variables?: TVariables) => Promise>; - fetchMore: ( - (fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions) => Promise> - ) & ( - (fetchMoreOptions: { query: DocumentNode } & FetchMoreQueryOptions & FetchMoreOptions) => Promise> - ); + fetchMore: (( + fetchMoreOptions: FetchMoreQueryOptions & + FetchMoreOptions, + ) => Promise>) & + (( + fetchMoreOptions: { query: DocumentNode } & FetchMoreQueryOptions< + TVariables2, + K + > & + FetchMoreOptions, + ) => Promise>); updateQuery: ( - mapFn: (previousQueryResult: TData, options: { variables?: TVariables }) => TData, + mapFn: ( + previousQueryResult: TData, + options: { variables?: TVariables }, + ) => TData, ) => void; }; -function observableQueryFields(observable: ObservableQuery): ObservableQueryFields { +function observableQueryFields( + observable: ObservableQuery, +): ObservableQueryFields { const fields = pick( observable, 'refetch', @@ -52,8 +69,13 @@ function observableQueryFields(observable: ObservableQuery { - const k = key as 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling'; + Object.keys(fields).forEach(key => { + const k = key as + | 'refetch' + | 'fetchMore' + | 'updateQuery' + | 'startPolling' + | 'stopPolling'; if (typeof fields[k] === 'function') { fields[k] = fields[k].bind(observable); } @@ -100,10 +122,10 @@ interface QueryContext { client: ApolloClient; } -class Query extends React.Component< - QueryProps, - QueryState -> { +class Query< + TData = any, + TVariables = OperationVariables +> extends React.Component, QueryState> { static contextTypes = { client: PropTypes.object.isRequired, }; @@ -155,7 +177,10 @@ class Query extends React.Componen this.startQuerySubscription(); } - componentWillReceiveProps(nextProps: QueryProps, nextContext: QueryContext) { + componentWillReceiveProps( + nextProps: QueryProps, + nextContext: QueryContext, + ) { if ( shallowEqual(this.props, nextProps) && this.client === nextContext.client @@ -182,7 +207,9 @@ class Query extends React.Componen return children(queryResult); } - private initializeQueryObservable = (props: QueryProps) => { + private initializeQueryObservable = ( + props: QueryProps, + ) => { const { variables, pollInterval, diff --git a/src/Subscriptions.tsx b/src/Subscriptions.tsx index d83c680c97..17fcc99624 100644 --- a/src/Subscriptions.tsx +++ b/src/Subscriptions.tsx @@ -16,7 +16,10 @@ export interface SubscriptionResult { error?: ApolloError; } -export interface SubscriptionProps { +export interface SubscriptionProps< + TData = any, + TVariables = OperationVariables +> { query: DocumentNode; variables?: TVariables; children: (result: SubscriptionResult) => React.ReactNode; @@ -44,7 +47,10 @@ class Subscription extends React.Component< private queryObservable: Observable; private querySubscription: ZenObservable.Subscription; - constructor(props: SubscriptionProps, context: SubscriptionContext) { + constructor( + props: SubscriptionProps, + context: SubscriptionContext, + ) { super(props, context); invariant( @@ -60,7 +66,10 @@ class Subscription extends React.Component< this.startSubscription(); } - componentWillReceiveProps(nextProps: SubscriptionProps, nextContext: SubscriptionContext) { + componentWillReceiveProps( + nextProps: SubscriptionProps, + nextContext: SubscriptionContext, + ) { if ( shallowEqual(this.props, nextProps) && this.client === nextContext.client diff --git a/src/getDataFromTree.ts b/src/getDataFromTree.ts index b622ec2f49..cb977355f7 100755 --- a/src/getDataFromTree.ts +++ b/src/getDataFromTree.ts @@ -5,7 +5,7 @@ import { Component, ComponentType, ComponentClass, - ChildContextProvider + ChildContextProvider, } from 'react'; import ApolloClient from 'apollo-client'; import assign from 'object-assign'; @@ -50,7 +50,9 @@ function isComponentClass( return Comp.prototype && Comp.prototype.render; } -function providesChildContext(instance: Component): instance is Component & ChildContextProvider { +function providesChildContext( + instance: Component, +): instance is Component & ChildContextProvider { return !!(instance as any).getChildContext; } @@ -101,7 +103,11 @@ export function walkTree( // 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); + newState = (newState as any)( + instance.state, + instance.props, + instance.context, + ); } instance.state = assign({}, instance.state, newState); }; @@ -158,10 +164,10 @@ export function walkTree( // TODO: Portals? } - - -function hasFetchDataFunction(instance: Component): instance is Component & { fetchData: () => Object } { - return typeof (instance as any).fetchData === 'function' +function hasFetchDataFunction( + instance: Component, +): instance is Component & { fetchData: () => Object } { + return typeof (instance as any).fetchData === 'function'; } function isPromise(query: Object): query is Promise { diff --git a/src/graphql.tsx b/src/graphql.tsx index c771a08ac3..007d1a3519 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -23,16 +23,19 @@ import hoistNonReactStatics from 'hoist-non-react-statics'; import shallowEqual from 'fbjs/lib/shallowEqual'; import invariant from 'invariant'; - const defaultMapPropsToOptions = () => ({}); const defaultMapResultToProps:

(props: P) => P = props => props; const defaultMapPropsToSkip = () => false; - -type ObservableQueryFields = Pick, 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling'>; +type ObservableQueryFields = Pick< + ObservableQuery, + 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling' +>; // the fields we want to copy over to our data prop -function observableQueryFields(observable: ObservableQuery): ObservableQueryFields { +function observableQueryFields( + observable: ObservableQuery, +): ObservableQueryFields { const fields = pick( observable, 'variables', @@ -45,7 +48,12 @@ function observableQueryFields(observable: ObservableQuery): Obser ); Object.keys(fields).forEach(key => { - const k = key as 'refetch' | 'fetchMore' | 'updateQuery' | 'startPolling' | 'stopPolling'; + const k = key as + | 'refetch' + | 'fetchMore' + | 'updateQuery' + | 'startPolling' + | 'stopPolling'; if (typeof fields[k] === 'function') { fields[k] = fields[k].bind(observable); } @@ -174,7 +182,10 @@ export default function graphql< } } - componentWillReceiveProps(nextProps: Readonly, nextContext: GraphqlContext) { + componentWillReceiveProps( + nextProps: Readonly, + nextContext: GraphqlContext, + ) { if (this.shouldSkip(nextProps)) { if (!this.shouldSkip(this.props)) { // if this has changed, we better unsubscribe @@ -651,9 +662,7 @@ export default function graphql< if (operationOptions.withRef) mergedPropsAndData.ref = this.setWrappedInstance; - this.renderedElement = ( - - ); + this.renderedElement = ; return this.renderedElement; } } diff --git a/src/withApollo.tsx b/src/withApollo.tsx index 72ae2bc392..ac4fc0a93f 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -11,7 +11,7 @@ function getDisplayName

(WrappedComponent: React.ComponentType

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

= P & { client: ApolloClient; }; +type WithApolloClient

= P & { client: ApolloClient }; export default function withApollo( WrappedComponent: React.ComponentType>, @@ -49,13 +49,12 @@ export default function withApollo( return ( {client => { - const props = Object.assign({}, - this.props, - { - client, - ref: operationOptions.withRef ? this.setWrappedInstance : undefined - } - ); + const props = Object.assign({}, this.props, { + client, + ref: operationOptions.withRef + ? this.setWrappedInstance + : undefined, + }); return ; }} diff --git a/test/client/Query.test.tsx b/test/client/Query.test.tsx index 13b64fb92d..0ec3338c8f 100644 --- a/test/client/Query.test.tsx +++ b/test/client/Query.test.tsx @@ -26,7 +26,9 @@ interface Data { }; } -const allPeopleData: Data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; +const allPeopleData: Data = { + allPeople: { people: [{ name: 'Luke Skywalker' }] }, +}; const allPeopleMocks = [ { request: { query: allPeopleQuery }, @@ -222,7 +224,6 @@ describe('Query component', () => { expect.assertions(5); - const Component = () => ( { let count = 0; expect.assertions(2); - const Component = () => ( {result => { @@ -316,15 +316,17 @@ describe('Query component', () => { result .fetchMore({ variables: { first: 1 }, - updateQuery: (prev, { fetchMoreResult }) => ( - fetchMoreResult ? { - allPeople: { - people: [ - ...prev.allPeople.people, - ...fetchMoreResult.allPeople.people, - ], - }, - } : prev), + updateQuery: (prev, { fetchMoreResult }) => + fetchMoreResult + ? { + allPeople: { + people: [ + ...prev.allPeople.people, + ...fetchMoreResult.allPeople.people, + ], + }, + } + : prev, }) .then(result2 => { expect(stripSymbols(result2.data)).toEqual(data2); @@ -1028,7 +1030,7 @@ describe('Query component', () => { function Container() { return ( - {(result) => { + {result => { try { switch (count++) { case 0: @@ -1046,11 +1048,9 @@ describe('Query component', () => { data.allPeople, ); setTimeout(() => { - result - .refetch() - .then(() => { - 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: @@ -1063,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 @@ -1098,7 +1096,7 @@ describe('Query component', () => { return null; }} - ) + ); } wrapper = mount( 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/server/getDataFromTree.test.tsx b/test/server/getDataFromTree.test.tsx index 55f2b140d2..149a47539a 100644 --- a/test/server/getDataFromTree.test.tsx +++ b/test/server/getDataFromTree.test.tsx @@ -126,7 +126,13 @@ describe('SSR', () => { it('functional stateless components with null children', () => { let elementCount = 0; - const MyComponent = ({ n, children = null }: { n: number, children: React.ReactNode }) => ( + const MyComponent = ({ + n, + children = null, + }: { + n: number; + children: React.ReactNode; + }) => (

{_.times(n, i => )} {children} @@ -345,7 +351,9 @@ describe('SSR', () => { const WrappedElement = () => ( {({ data, loading }) => ( -
{loading || !data ? 'loading' : data.currentUser.firstName}
+
+ {loading || !data ? 'loading' : data.currentUser.firstName} +
)}
); @@ -964,7 +972,9 @@ describe('SSR', () => { const Element = (props: { id: number }) => ( {({ data, loading }) => ( -
{loading || !data ? 'loading' : data.currentUser.firstName}
+
+ {loading || !data ? 'loading' : data.currentUser.firstName} +
)}
); diff --git a/test/typescript-usage.tsx b/test/typescript-usage.tsx index 86e9a1f9a0..c5bd8fae78 100644 --- a/test/typescript-usage.tsx +++ b/test/typescript-usage.tsx @@ -65,7 +65,11 @@ const withHistory = graphql(historyQuery, { class HistoryView extends React.Component> { render() { - if (this.props.data && this.props.data.history && 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; @@ -99,7 +103,11 @@ const HistoryViewSFC = graphql(historyQuery, { @graphql(historyQuery) class DecoratedHistoryView extends React.Component> { render(): React.ReactNode { - if (this.props.data && this.props.data.history && 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; @@ -112,9 +120,7 @@ class DecoratedHistoryView extends React.Component> { // -------------------------- // with custom props const withProps = graphql(historyQuery, { - props: ({ - data - }) => ({ + props: ({ data }) => ({ ...data, }), }); From 8c780103adeda502ccc25607812c9947162dee37 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Mon, 29 Jan 2018 18:05:46 -0300 Subject: [PATCH 07/14] Fix type checking in tests. Simplify props in graphql HoC --- package.json | 1 + src/graphql.tsx | 10 +- src/types.ts | 9 +- test/server/getDataFromTree.test.tsx | 299 +++++++++++++++++++-------- test/server/server.test.tsx | 91 +++++--- test/test-utils.test.tsx | 71 +++++-- test/test-utils/catchAsyncError.ts | 2 +- test/test-utils/createClient.ts | 7 +- test/test-utils/shim.ts | 5 +- test/test-utils/stripSymbols.ts | 2 +- test/test-utils/wait.ts | 2 +- test/test-utils/wrap.ts | 7 +- test/typescript-usage.tsx | 26 +-- yarn.lock | 6 + 14 files changed, 364 insertions(+), 174 deletions(-) diff --git a/package.json b/package.json index 2929c195ba..d6290682b8 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@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/zen-observable": "0.5.3", "apollo-cache-inmemory": "1.1.5", "apollo-client": "2.2.0", diff --git a/src/graphql.tsx b/src/graphql.tsx index 007d1a3519..943801abf7 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -70,7 +70,7 @@ function getDisplayName

(WrappedComponent: React.ComponentType

) { let nextVersion = 0; export default function graphql< - TProps = {}, + TProps extends TGraphQLVariables | {} = {}, TData = {}, TGraphQLVariables = {}, TChildProps extends TProps = ChildProps @@ -105,7 +105,7 @@ export default function graphql< ) { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; - type GraphqlProps = TProps & TGraphQLVariables; + type GraphqlProps = TProps; interface GraphqlContext { client: ApolloClient; @@ -183,7 +183,7 @@ export default function graphql< } componentWillReceiveProps( - nextProps: Readonly, + nextProps: GraphqlProps, nextContext: GraphqlContext, ) { if (this.shouldSkip(nextProps)) { @@ -272,7 +272,7 @@ export default function graphql< ); } - getClient(props: Readonly): ApolloClient { + getClient(props: GraphqlProps): ApolloClient { if (this.client) return this.client; const { client } = mapPropsToOptions(props); @@ -341,7 +341,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, }; diff --git a/src/types.ts b/src/types.ts index 79eaa14bc0..fc2c6a4ff4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -109,7 +109,7 @@ export interface OptionProps< > extends Partial>, Partial> { - ownProps: Readonly; + ownProps: TProps; } export interface OperationOption< @@ -121,15 +121,12 @@ export interface OperationOption< | QueryOpts | MutationOpts | (( - props: Readonly, + props: TProps, ) => QueryOpts | MutationOpts); props?: (props: OptionProps) => any; skip?: boolean | ((props: any) => boolean); name?: string; withRef?: boolean; - shouldResubscribe?: ( - props: Readonly, - nextProps: Readonly, - ) => boolean; + shouldResubscribe?: (props: TProps, nextProps: TProps) => boolean; alias?: string; } diff --git a/test/server/getDataFromTree.test.tsx b/test/server/getDataFromTree.test.tsx index 149a47539a..6cb926b6e8 100644 --- a/test/server/getDataFromTree.test.tsx +++ b/test/server/getDataFromTree.test.tsx @@ -6,7 +6,6 @@ import { graphql, Query, ApolloProvider, - DataValue, walkTree, getDataFromTree, } from '../../src'; @@ -15,6 +14,7 @@ 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`', () => { @@ -303,9 +303,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} +
), ); @@ -394,10 +398,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 = ( @@ -436,10 +444,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 = ( @@ -479,9 +491,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} +
), ); @@ -507,7 +523,7 @@ describe('SSR', () => { }); it('should handle nested queries that depend on each other', () => { - const idQuery = gql` + const idQuery: DocumentNode = gql` { currentUser { id @@ -515,7 +531,7 @@ describe('SSR', () => { } `; const idData = { currentUser: { id: '1234' } }; - const userQuery = gql` + const userQuery: DocumentNode = gql` query getUser($id: String) { user(id: $id) { firstName @@ -538,22 +554,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 = ( @@ -592,9 +636,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}
), ); @@ -649,7 +693,7 @@ describe('SSR', () => { firstName: string; }; } - const WrappedElement = graphql(query, { + const WrappedElement = graphql(query, { skip: true, })(({ data }: ChildProps) => (
{!data ? 'skipped' : 'dang'}
@@ -675,11 +719,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 }); @@ -688,28 +732,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(); }); }); @@ -721,11 +776,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, }); @@ -735,8 +790,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() { @@ -744,17 +813,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 = ( - + ); @@ -763,7 +838,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(); }) @@ -789,9 +864,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' }; } @@ -813,11 +887,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({ @@ -827,22 +901,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 = { @@ -853,7 +930,11 @@ describe('SSR', () => { componentWillMount() { this.setState( - (state, props, context) => + ( + state: State, + props: Props, + context: { client: ApolloClient }, + ) => ({ thing: state.thing + 1, userId: props.id, @@ -863,19 +944,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 = ( - + ); @@ -884,7 +971,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(); }) @@ -899,11 +986,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, }); @@ -919,16 +1006,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 = ( - + ); @@ -948,7 +1050,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 }, @@ -967,9 +1069,9 @@ describe('SSR', () => { }; } - class CurrentUserQuery extends Query {} + class CurrentUserQuery extends Query {} - const Element = (props: { id: number }) => ( + const Element = (props: { id: string }) => ( {({ data, loading }) => (
@@ -981,7 +1083,7 @@ describe('SSR', () => { const app = ( - + ); @@ -1024,36 +1126,47 @@ 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 MutationProps { + refetchQuery: Function; + data: Data; + } + + const withQuery = graphql(query, { + options: () => ({ ssr: true }), props: ({ data }) => { - expect(data.refetch).toBeTruthy(); + expect(data!.refetch).toBeTruthy(); return { - refetchQuery: data.refetch, + refetchQuery: data!.refetch, data, }; }, }); - interface MutationProps { - refetchQuery: Function; - data: Data; - } const withMutation = graphql(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> = ({ + data, + }) => ( +
+ {data.loading || !data.currentUser + ? 'loading' + : data.currentUser.firstName} +
); const WrappedElement = withQuery(withMutation(Element)); @@ -1129,8 +1242,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)); @@ -1171,14 +1290,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/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 c5bd8fae78..d53c953423 100644 --- a/test/typescript-usage.tsx +++ b/test/typescript-usage.tsx @@ -121,23 +121,25 @@ class DecoratedHistoryView extends React.Component> { // with custom props const withProps = graphql(historyQuery, { props: ({ data }) => ({ - ...data, + organisationData: data, }), }); // -------------------------- +// 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 +// 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/yarn.lock b/yarn.lock index 2ceace01c8..dcdb44e279 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,6 +77,12 @@ "@types/node" "*" "@types/react" "*" +"@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" From e83f270012351cf9dcff64526319cfdf9d39c510 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Tue, 30 Jan 2018 14:45:27 -0300 Subject: [PATCH 08/14] Fix a lot of tests. Tweak graphql HoC types to accomodate more use cases --- src/ApolloConsumer.tsx | 2 +- src/graphql.tsx | 19 +- src/types.ts | 20 +- src/withApollo.tsx | 13 +- test/client/ApolloConsumer.test.tsx | 4 +- test/client/ApolloProvider.test.tsx | 16 +- test/client/Subscription.test.tsx | 12 +- test/client/graphql/mutations/index.test.tsx | 79 ++++--- .../graphql/mutations/lifecycle.test.tsx | 51 ++-- .../client/graphql/mutations/queries.test.tsx | 218 +++++++++++------- .../mutations/recycled-queries.test.tsx | 101 +++++--- .../client/graphql/shared-operations.test.tsx | 6 +- test/client/graphql/statics.test.tsx | 11 +- test/server/getDataFromTree.test.tsx | 58 ++--- test/typescript-usage.tsx | 17 +- typings/hoist-non-react-statics.d.ts | 11 +- 16 files changed, 400 insertions(+), 238 deletions(-) diff --git a/src/ApolloConsumer.tsx b/src/ApolloConsumer.tsx index 25b8856fef..cad809a8e1 100644 --- a/src/ApolloConsumer.tsx +++ b/src/ApolloConsumer.tsx @@ -4,7 +4,7 @@ import ApolloClient from 'apollo-client'; 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/graphql.tsx b/src/graphql.tsx index 943801abf7..788021d71b 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -9,12 +9,13 @@ import { parser, DocumentType } from './parser'; import { DocumentNode } from 'graphql'; import { MutationOpts, - ChildProps, OperationOption, QueryOpts, GraphqlQueryControls, MutationFunc, OptionProps, + DataProps, + MutateProps, } from './types'; import { OperationVariables } from './index'; import pick from 'lodash/pick'; @@ -73,10 +74,16 @@ export default function graphql< TProps extends TGraphQLVariables | {} = {}, TData = {}, TGraphQLVariables = {}, - TChildProps extends TProps = ChildProps + TChildProps = Partial> & + Partial> >( document: DocumentNode, - operationOptions: OperationOption = {}, + operationOptions: OperationOption< + TProps, + TData, + TGraphQLVariables, + TChildProps + > = {}, ) { // extract options const { @@ -101,7 +108,7 @@ export default function graphql< const version = nextVersion++; function wrapWithApolloComponent( - WrappedComponent: React.ComponentType, + WrappedComponent: React.ComponentType, ) { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; @@ -543,7 +550,7 @@ export default function graphql< this.wrappedInstance = ref; } - dataForChildViaMutation(mutationOpts: MutationOpts) { + dataForChildViaMutation(mutationOpts?: MutationOpts) { const opts = this.calculateOptions(this.props, mutationOpts); if (typeof opts.variables === 'undefined') delete opts.variables; @@ -661,7 +668,7 @@ export default function graphql< const mergedPropsAndData = assign({}, props, clientProps); if (operationOptions.withRef) - mergedPropsAndData.ref = this.setWrappedInstance; + (mergedPropsAndData as any).ref = this.setWrappedInstance; this.renderedElement = ; return this.renderedElement; } diff --git a/src/types.ts b/src/types.ts index fc2c6a4ff4..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,15 +118,18 @@ 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; diff --git a/src/withApollo.tsx b/src/withApollo.tsx index ac4fc0a93f..b4795ab6ec 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -2,10 +2,9 @@ import * as React from 'react'; import { OperationOption } from './types'; import ApolloConsumer from './ApolloConsumer'; import { ApolloClient } from 'apollo-client'; - -const invariant = require('invariant'); - -const hoistNonReactStatics = require('hoist-non-react-statics'); +import assign from 'object-assign'; +import invariant from 'invariant'; +import hoistNonReactStatics from 'hoist-non-react-statics'; function getDisplayName

      (WrappedComponent: React.ComponentType

      ) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; @@ -19,14 +18,14 @@ export default function withApollo( ): 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: WithApolloClient) { + constructor(props: TProps) { super(props); this.setWrappedInstance = this.setWrappedInstance.bind(this); } @@ -49,7 +48,7 @@ export default function withApollo( return ( {client => { - const props = Object.assign({}, this.props, { + const props = assign({}, this.props, { client, ref: operationOptions.withRef ? this.setWrappedInstance 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/Subscription.test.tsx b/test/client/Subscription.test.tsx index 1c1001299b..5ea5605960 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, RequestHandler, Operation, Observable } 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/graphql/mutations/index.test.tsx b/test/client/graphql/mutations/index.test.tsx index 1c8eeb4985..c4c8f0cda8 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(); @@ -106,14 +116,14 @@ describe('graphql(mutation)', () => { it('can execute a mutation', done => { @graphql(query) - class Container extends React.Component { + class Container extends React.Component { componentDidMount() { - this.props.mutate().then(result => { + this.props.mutate!().then(result => { expect(stripSymbols(result.data)).toEqual(expectedData); done(); }); } - render() { + render(): React.ReactNode { return null; } } @@ -136,10 +146,15 @@ describe('graphql(mutation)', () => { } `; client = createClient(expectedData, queryWithVariables, { first: 1 }); - @graphql(queryWithVariables) - class Container extends React.Component { + + interface Props { + first: number; + } + + @graphql(queryWithVariables) + class Container extends React.Component> { componentDidMount() { - this.props.mutate().then(result => { + this.props.mutate!().then(result => { expect(stripSymbols(result.data)).toEqual(expectedData); done(); }); diff --git a/test/client/graphql/mutations/lifecycle.test.tsx b/test/client/graphql/mutations/lifecycle.test.tsx index b7a56abfc8..16358f9b98 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,10 +22,14 @@ 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 { + interface Props { + id: string | null; + } + + @graphql(query) + class Container extends React.Component> { componentDidMount() { - this.props.mutate().then(result => { + this.props.mutate!().then(result => { expect(stripSymbols(result.data)).toEqual(expectedData); done(); }); @@ -45,7 +49,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 +66,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 +100,7 @@ describe('graphql(mutation) lifecycle', () => { } render() { - return ; + return ; } } @@ -100,10 +113,14 @@ 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 { + interface Variables { + id: number; + } + + @graphql<{}, {}, Variables>(query) + class Container extends React.Component> { componentDidMount() { - this.props.mutate({ variables: { id: 1 } }).then(result => { + this.props.mutate!({ variables: { id: 1 } }).then(result => { expect(stripSymbols(result.data)).toEqual(expectedData); done(); }); diff --git a/test/client/graphql/mutations/queries.test.tsx b/test/client/graphql/mutations/queries.test.tsx index 0a45f614fb..aae8d5fd92 100644 --- a/test/client/graphql/mutations/queries.test.tsx +++ b/test/client/graphql/mutations/queries.test.tsx @@ -1,7 +1,7 @@ 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 { @@ -12,12 +12,12 @@ import { } from '../../../../src'; import stripSymbols from '../../../test-utils/stripSymbols'; import createClient from '../../../test-utils/createClient'; - -const compose = require('lodash/flowRight'); +import { DocumentNode } from 'graphql'; +import compose from 'lodash/flowRight'; 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,9 +38,12 @@ describe('graphql(mutation) query integration', () => { completed: true, }, }; + + type Data = typeof data; + const client = createClient(data, query); - @graphql(query) - class Container extends React.Component { + @graphql<{}, Data>(query) + class Container extends React.Component> { componentDidMount() { const optimisticResponse = { __typename: 'Mutation', @@ -51,7 +54,7 @@ describe('graphql(mutation) query integration', () => { completed: true, }, }; - this.props.mutate({ optimisticResponse }).then(result => { + this.props.mutate!({ optimisticResponse }).then(result => { expect(stripSymbols(result.data)).toEqual(data); done(); }); @@ -74,7 +77,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 +91,7 @@ describe('graphql(mutation) query integration', () => { } `; - const mutation = gql` + const mutation: DocumentNode = gql` mutation createTodo { createTodo { id @@ -106,6 +109,8 @@ describe('graphql(mutation) query integration', () => { }, }; + type MutationData = typeof mutationData; + const optimisticResponse = { createTodo: { id: '99', @@ -113,10 +118,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 +146,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 +188,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 +215,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 +239,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 +256,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 +265,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 +319,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 +331,7 @@ describe('graphql(mutation) query integration', () => { } `; - const overlappingQuery = gql` + const overlappingQuery: DocumentNode = gql` query Account__PaymentQuery($accountId: ID!) { account(id: $accountId) { id @@ -323,7 +362,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 +378,13 @@ describe('graphql(mutation) query integration', () => { accountSetPlan: true, }; + type MutationData = typeof mutationData; + + interface MutationVariables { + accountId: string; + planId: string; + } + const variables = { accountId, }; @@ -362,7 +414,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 +422,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 +439,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 +474,7 @@ describe('graphql(mutation) query integration', () => { async onPaymentInfoChanged() { try { refetched = true; - await this.props.setPlan({ + await this.props.mutate!({ refetchQueries: [ { query: billingInfoQuery, @@ -433,17 +505,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..6da8a3e871 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,12 +101,12 @@ describe('graphql(mutation) update queries', () => { cache: new Cache({ addTypename: false }), }); - let mutate; + let mutate: MutationFunc; - @graphql(mutation, { options: () => ({ update }) }) - class MyMutation extends React.Component { + @graphql<{}, MutationData>(mutation, { options: () => ({ update }) }) + class MyMutation extends React.Component> { componentDidMount() { - mutate = this.props.mutate; + mutate = this.props.mutate!; } render() { @@ -103,8 +118,8 @@ describe('graphql(mutation) update queries', () => { let queryUnmountCount = 0; let queryRenderCount = 0; - @graphql(query) - class MyQuery extends React.Component { + @graphql<{}, QueryData>(query) + class MyQuery extends React.Component> { componentWillMount() { queryMountCount++; } @@ -117,11 +132,11 @@ describe('graphql(mutation) update queries', () => { try { switch (queryRenderCount++) { case 0: - expect(this.props.data.loading).toBeTruthy(); - expect(this.props.data.todo_list).toBeFalsy(); + expect(this.props.data!.loading).toBeTruthy(); + expect(this.props.data!.todo_list).toBeFalsy(); break; case 1: - expect(stripSymbols(this.props.data.todo_list)).toEqual({ + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ id: '123', title: 'how to apollo', tasks: [], @@ -130,7 +145,7 @@ describe('graphql(mutation) update queries', () => { case 2: expect(queryMountCount).toBe(1); expect(queryUnmountCount).toBe(0); - expect(stripSymbols(this.props.data.todo_list)).toEqual({ + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ id: '123', title: 'how to apollo', tasks: [ @@ -145,7 +160,7 @@ describe('graphql(mutation) update queries', () => { case 3: expect(queryMountCount).toBe(2); expect(queryUnmountCount).toBe(1); - expect(stripSymbols(this.props.data.todo_list)).toEqual({ + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ id: '123', title: 'how to apollo', tasks: [ @@ -163,7 +178,7 @@ describe('graphql(mutation) update queries', () => { }); break; case 4: - expect(stripSymbols(this.props.data.todo_list)).toEqual({ + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ id: '123', title: 'how to apollo', tasks: [ @@ -221,7 +236,7 @@ describe('graphql(mutation) update queries', () => { setTimeout(() => { const wrapperQuery2 = renderer.create( - + , ); @@ -248,7 +263,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 +281,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 +297,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,12 +334,12 @@ describe('graphql(mutation) update queries', () => { cache: new Cache({ addTypename: false }), }); - let mutate; + let mutate: MutationFunc; - @graphql(mutation, {}) - class Mutation extends React.Component { + @graphql<{}, MutationData>(mutation) + class Mutation extends React.Component> { componentDidMount() { - mutate = this.props.mutate; + mutate = this.props.mutate!; } render() { @@ -322,8 +351,10 @@ describe('graphql(mutation) update queries', () => { let queryUnmountCount = 0; let queryRenderCount = 0; - @graphql(query) - class Query extends React.Component { + @graphql(query) + class Query extends React.Component< + ChildProps + > { componentWillMount() { queryMountCount++; } @@ -336,12 +367,12 @@ describe('graphql(mutation) update queries', () => { try { switch (queryRenderCount++) { case 0: - expect(this.props.data.loading).toBeTruthy(); - expect(this.props.data.todo_list).toBeFalsy(); + 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({ + expect(this.props.data!.loading).toBeFalsy(); + expect(stripSymbols(this.props.data!.todo_list)).toEqual({ id: '123', title: 'how to apollo', tasks: [], @@ -350,14 +381,14 @@ describe('graphql(mutation) update queries', () => { case 2: expect(queryMountCount).toBe(2); expect(queryUnmountCount).toBe(1); - expect(stripSymbols(this.props.data.todo_list)).toEqual( + 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( + expect(stripSymbols(this.props.data!.todo_list)).toEqual( updatedData.todo_list, ); break; @@ -387,7 +418,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/shared-operations.test.tsx b/test/client/graphql/shared-operations.test.tsx index 93081f99dd..3f0d5e017c 100644 --- a/test/client/graphql/shared-operations.test.tsx +++ b/test/client/graphql/shared-operations.test.tsx @@ -20,13 +20,13 @@ 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(), }); diff --git a/test/client/graphql/statics.test.tsx b/test/client/graphql/statics.test.tsx index 36be24fc67..50d3bb1a2d 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', () => { diff --git a/test/server/getDataFromTree.test.tsx b/test/server/getDataFromTree.test.tsx index 6cb926b6e8..7addca6275 100644 --- a/test/server/getDataFromTree.test.tsx +++ b/test/server/getDataFromTree.test.tsx @@ -8,6 +8,7 @@ import { ApolloProvider, walkTree, getDataFromTree, + DataValue, } from '../../src'; import gql from 'graphql-tag'; import * as _ from 'lodash'; @@ -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); @@ -83,7 +84,7 @@ describe('SSR', () => { const MyComponent = ({ n }: { n: number }) => (
      {_.times(n, i => )}
      ); - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -138,7 +139,7 @@ describe('SSR', () => { {children}
      ); - walkTree({null}, {}, element => { + walkTree({null}, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -147,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); @@ -156,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); @@ -166,7 +167,7 @@ describe('SSR', () => { let elementCount = 0; const MyComponent = () => [null,
      ] as any; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -176,7 +177,7 @@ describe('SSR', () => { let elementCount = 0; const MyComponent = () => [undefined,
      ] as any; - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -189,7 +190,7 @@ describe('SSR', () => { return
      {_.times(this.props.n, i => )}
      ; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -202,7 +203,7 @@ describe('SSR', () => { return null; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(1); @@ -215,7 +216,7 @@ describe('SSR', () => { return [1, 2]; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(3); @@ -229,7 +230,7 @@ describe('SSR', () => { return [null,
      ]; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(2); @@ -245,7 +246,7 @@ describe('SSR', () => { return
      {_.times(this.props.n, i => )}
      ; } } - walkTree(, {}, element => { + walkTree(, {}, () => { elementCount += 1; }); expect(elementCount).toEqual(7); @@ -268,7 +269,7 @@ describe('SSR', () => { Foo , {}, - element => { + () => { elementCount += 1; }, ); @@ -1132,23 +1133,28 @@ describe('SSR', () => { }; } interface QueryProps {} - interface MutationProps { + interface QueryChildProps { refetchQuery: Function; - data: Data; + data: DataValue; } - const withQuery = graphql(query, { + const withQuery = graphql(query, { options: () => ({ ssr: true }), props: ({ data }) => { expect(data!.refetch).toBeTruthy(); return { refetchQuery: data!.refetch, - data, + data: data!, }; }, }); - const withMutation = graphql(mutation, { + const withMutation = graphql< + QueryChildProps, + {}, + {}, + { action: (variables: {}) => Promise } + >(mutation, { props: ({ ownProps, mutate }) => { expect(ownProps.refetchQuery).toBeTruthy(); return { @@ -1159,9 +1165,9 @@ describe('SSR', () => { }, }); - const Element: React.StatelessComponent> = ({ - data, - }) => ( + const Element: React.StatelessComponent< + QueryChildProps & { action: (variables: {}) => Promise } + > = ({ data }) => (
      {data.loading || !data.currentUser ? 'loading' diff --git a/test/typescript-usage.tsx b/test/typescript-usage.tsx index d53c953423..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) { @@ -119,12 +119,23 @@ class DecoratedHistoryView extends React.Component> { // -------------------------- // with custom props -const withProps = graphql(historyQuery, { +const withProps = graphql< + Props, + Data, + {}, + { organisationData: DataValue | undefined } +>(historyQuery, { props: ({ data }) => ({ organisationData: data, }), }); +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. diff --git a/typings/hoist-non-react-statics.d.ts b/typings/hoist-non-react-statics.d.ts index ef25efdd89..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: React.ComponentClass, - sourceComponent: React.ComponentType, + function hoistNonReactStatics< + TTargetComponent extends React.ComponentType, + TSourceComponent extends React.ComponentType + >( + targetComponent: TTargetComponent, + sourceComponent: TSourceComponent, customStatics: { [name: string]: boolean }, - ): React.ComponentClass; + ): TTargetComponent; namespace hoistNonReactStatics { } From b3e4fea01f27f470f9a71f888cd448ad11110fc2 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Tue, 30 Jan 2018 16:10:42 -0300 Subject: [PATCH 09/14] Type check more tests --- test/client/graphql/client-option.test.tsx | 59 +++--- test/client/graphql/fragments.test.tsx | 60 +++--- .../client/graphql/shared-operations.test.tsx | 132 ++++++++----- test/client/graphql/subscriptions.test.tsx | 178 ++++++++++-------- 4 files changed, 261 insertions(+), 168 deletions(-) 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/shared-operations.test.tsx b/test/client/graphql/shared-operations.test.tsx index 3f0d5e017c..22140d7595 100644 --- a/test/client/graphql/shared-operations.test.tsx +++ b/test/client/graphql/shared-operations.test.tsx @@ -13,8 +13,8 @@ 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', () => { @@ -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; + } + + // 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 ComposedProps { - people?: DataValue; - ships?: DataValue; + 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 { @@ -302,7 +327,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 +399,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 +409,8 @@ describe('shared operations', () => { } `; const data = { allPeople: { people: [{ name: 'Luke Skywalker' }] } }; + type Data = typeof data; + const link = mockSingleLink({ request: { query }, result: { data }, @@ -393,17 +420,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 +450,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 +463,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 +476,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 +487,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/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 { From 3523a8e5440de189c0d243ab6225e44013a57248 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Tue, 30 Jan 2018 17:27:53 -0300 Subject: [PATCH 10/14] More tests type checked --- package.json | 46 ++- src/graphql.tsx | 2 +- test/client/graphql/queries/api.test.tsx | 274 +++++++------- test/client/graphql/queries/errors.test.tsx | 324 +++++++++-------- test/client/graphql/queries/index.test.tsx | 277 ++++++++------ .../client/graphql/queries/lifecycle.test.tsx | 343 ++++++++++-------- yarn.lock | 6 + 7 files changed, 724 insertions(+), 548 deletions(-) diff --git a/package.json b/package.json index d6290682b8..9f5f8dcc05 100644 --- a/package.json +++ b/package.json @@ -19,19 +19,14 @@ "type-check": "tsc --project tsconfig.json --noEmit && flow check", "precompile": "rimraf lib", "compile": "npm run compile:esm && npm run compile:cjs", - "compile:esm": - "tsc --project tsconfig.json -d && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src && cd lib && rename js mjs", - "compile:cjs": - "tsc --project tsconfig.cjs.json && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src", - "postcompile": - "rollup -c rollup.config.js && rollup -c rollup.browser.config.js && ./scripts/prepare-package.sh", + "compile:esm": "tsc --project tsconfig.json -d && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src && cd lib && rename js mjs", + "compile:cjs": "tsc --project tsconfig.cjs.json && rimraf lib/test && mv lib/src/* lib/. && rimraf lib/src", + "postcompile": "rollup -c rollup.config.js && rollup -c rollup.browser.config.js && ./scripts/prepare-package.sh", "watch": "tsc -w", "lint": "tslint --project tsconfig.json --config tslint.json", - "lint:fix": - "npm run prettier && tslint 'src/*.ts*' --project tsconfig.json --fix", + "lint:fix": "npm run prettier && tslint 'src/*.ts*' --project tsconfig.json --fix", "lint-staged": "lint-staged", - "prettier": - "prettier --write \"{,!(node_modules|lib|coverage|npm)/**/}*.{ts*,js*,json,md}\"" + "prettier": "prettier --write \"{,!(node_modules|lib|coverage|npm)/**/}*.{ts*,js*,json,md}\"" }, "bundlesize": [ { @@ -40,8 +35,15 @@ } ], "lint-staged": { - "*.{ts*}": ["prettier --write", "npm run lint", "git add"], - "*.{js*,json,md}": ["prettier --write", "git add"] + "*.{ts*}": [ + "prettier --write", + "npm run lint", + "git add" + ], + "*.{js*,json,md}": [ + "prettier --write", + "git add" + ] }, "repository": { "type": "git", @@ -58,7 +60,9 @@ ], "author": "James Baxley ", "babel": { - "presets": ["env"] + "presets": [ + "env" + ] }, "jest": { "testEnvironment": "jsdom", @@ -67,15 +71,24 @@ "^.+\\.jsx?$": "babel-jest" }, "mapCoverage": true, - "moduleFileExtensions": ["ts", "tsx", "js", "json"], + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "json" + ], "modulePathIgnorePatterns": [ "/examples", "/test/flow-usage.js", "/test/typescript-usage.tsx" ], - "projects": [""], + "projects": [ + "" + ], "testRegex": "(/test/(?!test-utils\b)\b.*|\\.(test|spec))\\.(ts|tsx|js)$", - "setupFiles": ["/test/test-utils/setup.ts"] + "setupFiles": [ + "/test/test-utils/setup.ts" + ] }, "license": "MIT", "peerDependencies": { @@ -94,6 +107,7 @@ "@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.0", diff --git a/src/graphql.tsx b/src/graphql.tsx index 788021d71b..66a17adc51 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -109,7 +109,7 @@ export default function graphql< function wrapWithApolloComponent( WrappedComponent: React.ComponentType, - ) { + ): React.ComponentClass { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; type GraphqlProps = TProps; diff --git a/test/client/graphql/queries/api.test.tsx b/test/client/graphql/queries/api.test.tsx index 8d86cfb56d..fffecb4b33 100644 --- a/test/client/graphql/queries/api.test.tsx +++ b/test/client/graphql/queries/api.test.tsx @@ -13,11 +13,12 @@ import { 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 +39,7 @@ describe('[queries] api', () => { cache: new Cache({ addTypename: false }), }); - let hasRefetched, + let hasRefetched = false, count = 0; interface Props { @@ -48,45 +49,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 +100,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 +119,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 +141,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 +152,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 +170,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 +227,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 +245,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 +260,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 +292,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/yarn.lock b/yarn.lock index dcdb44e279..d5b81466ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,6 +87,12 @@ version "16.0.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.35.tgz#7ce8a83cad9690fd965551fc513217a74fc9e079" +"@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" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.3.tgz#91b728599544efbb7386d8b6633693a3c2e7ade5" From 06637ea0f40e27f27c867604621b24664fa5bf1e Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Thu, 1 Feb 2018 10:35:27 -0300 Subject: [PATCH 11/14] Fix all remaining type errors in tests --- src/test-links.ts | 2 +- test/client/Subscription.test.tsx | 2 +- test/client/graphql/mutations/index.test.tsx | 50 +- .../graphql/mutations/lifecycle.test.tsx | 52 +- .../client/graphql/mutations/queries.test.tsx | 59 ++- .../mutations/recycled-queries.test.tsx | 298 ++++++------ test/client/graphql/queries/api.test.tsx | 7 +- test/client/graphql/queries/loading.test.tsx | 453 +++++++++-------- .../graphql/queries/observableQuery.test.tsx | 204 ++++---- test/client/graphql/queries/polling.test.tsx | 74 +-- test/client/graphql/queries/reducer.test.tsx | 51 +- test/client/graphql/queries/skip.test.tsx | 458 ++++++++++-------- .../graphql/queries/updateQuery.test.tsx | 192 ++++---- .../client/graphql/shared-operations.test.tsx | 26 +- test/client/graphql/statics.test.tsx | 9 +- 15 files changed, 1052 insertions(+), 885 deletions(-) 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/test/client/Subscription.test.tsx b/test/client/Subscription.test.tsx index 5ea5605960..75633d56d2 100644 --- a/test/client/Subscription.test.tsx +++ b/test/client/Subscription.test.tsx @@ -3,7 +3,7 @@ import gql from 'graphql-tag'; import { mount, ReactWrapper } from 'enzyme'; import { ApolloClient } from 'apollo-client'; -import { ApolloLink, RequestHandler, Operation, Observable } from 'apollo-link'; +import { ApolloLink, Operation } from 'apollo-link'; import { InMemoryCache as Cache } from 'apollo-cache-inmemory'; import { MockSubscriptionLink } from '../../src/test-utils'; diff --git a/test/client/graphql/mutations/index.test.tsx b/test/client/graphql/mutations/index.test.tsx index c4c8f0cda8..bf9acd8136 100644 --- a/test/client/graphql/mutations/index.test.tsx +++ b/test/client/graphql/mutations/index.test.tsx @@ -115,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(): React.ReactNode { - 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( @@ -151,18 +152,19 @@ describe('graphql(mutation)', () => { first: number; } - @graphql(queryWithVariables) - class Container extends React.Component> { - componentDidMount() { - this.props.mutate!().then(result => { - expect(stripSymbols(result.data)).toEqual(expectedData); - done(); - }); - } - render() { - return null; - } - } + 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 16358f9b98..0cdc3583d8 100644 --- a/test/client/graphql/mutations/lifecycle.test.tsx +++ b/test/client/graphql/mutations/lifecycle.test.tsx @@ -26,19 +26,20 @@ describe('graphql(mutation) lifecycle', () => { id: string | null; } - @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( @@ -117,18 +118,19 @@ describe('graphql(mutation) lifecycle', () => { id: number; } - @graphql<{}, {}, Variables>(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; - } - } + 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 aae8d5fd92..02b0372d15 100644 --- a/test/client/graphql/mutations/queries.test.tsx +++ b/test/client/graphql/mutations/queries.test.tsx @@ -4,16 +4,10 @@ import gql from 'graphql-tag'; 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'; import { DocumentNode } from 'graphql'; -import compose from 'lodash/flowRight'; describe('graphql(mutation) query integration', () => { it('allows for passing optimisticResponse for a mutation', done => { @@ -42,32 +36,33 @@ describe('graphql(mutation) query integration', () => { type Data = typeof data; const client = createClient(data, query); - @graphql<{}, Data>(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( diff --git a/test/client/graphql/mutations/recycled-queries.test.tsx b/test/client/graphql/mutations/recycled-queries.test.tsx index 6da8a3e871..0d911d29f4 100644 --- a/test/client/graphql/mutations/recycled-queries.test.tsx +++ b/test/client/graphql/mutations/recycled-queries.test.tsx @@ -103,107 +103,111 @@ describe('graphql(mutation) update queries', () => { let mutate: MutationFunc; - @graphql<{}, MutationData>(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<{}, QueryData>(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( @@ -336,71 +340,73 @@ describe('graphql(mutation) update queries', () => { let mutate: MutationFunc; - @graphql<{}, MutationData>(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< - ChildProps - > { - 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( diff --git a/test/client/graphql/queries/api.test.tsx b/test/client/graphql/queries/api.test.tsx index fffecb4b33..f43f512c79 100644 --- a/test/client/graphql/queries/api.test.tsx +++ b/test/client/graphql/queries/api.test.tsx @@ -4,12 +4,7 @@ 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'; 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..165fce48da 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,10 @@ describe('[queries] observableQuery', () => { }); let remount: any; - class Remounter extends React.Component { + class Remounter extends React.Component< + { render: typeof Container }, + { showChildren: boolean; variables: Vars } + > { state = { showChildren: true, variables: variables1, @@ -455,32 +472,33 @@ 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); + 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); + 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); } - } catch (e) { - done.fail(e); - } - return null; - } - } + return null; + } + }, + ); // the initial mount fires off the query // the same as episode id = 1 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 22140d7595..a27ef9c0f2 100644 --- a/test/client/graphql/shared-operations.test.tsx +++ b/test/client/graphql/shared-operations.test.tsx @@ -305,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( diff --git a/test/client/graphql/statics.test.tsx b/test/client/graphql/statics.test.tsx index 50d3bb1a2d..77c72de490 100644 --- a/test/client/graphql/statics.test.tsx +++ b/test/client/graphql/statics.test.tsx @@ -31,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)'); }); From 605309bf9d6c3555b103c112ad3012fecc96aa00 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Thu, 1 Feb 2018 10:54:11 -0300 Subject: [PATCH 12/14] Changelog entry --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index 1a24315473..7afbd55b45 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,9 @@ ### vNext +* Stricter type checking in the codebase. [#1617] +* Improved TS types (even more) in both `Query` component and `graphql` HoC. [#1617] + ### 2.1.0-beta.0 * Beta release of all 2.1 features! From 9e9341d9d1de444e48dc506c30214bc4abd9c078 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Thu, 1 Feb 2018 11:55:03 -0300 Subject: [PATCH 13/14] Export types for Typescript emission of type declarations --- src/Query.tsx | 16 ++++++---------- src/Subscriptions.tsx | 2 +- src/withApollo.tsx | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Query.tsx b/src/Query.tsx index 611d07f994..5e6f4ca4ef 100644 --- a/src/Query.tsx +++ b/src/Query.tsx @@ -17,7 +17,7 @@ import shallowEqual from 'fbjs/lib/shallowEqual'; import invariant from 'invariant'; // Improved FetchMoreOptions type, need to port them back to Apollo Client -interface FetchMoreOptions { +export interface FetchMoreOptions { updateQuery: ( previousQueryResult: TData, options: { @@ -28,12 +28,12 @@ interface FetchMoreOptions { } // Improved FetchMoreQueryOptions type, need to port them back to Apollo Client -interface FetchMoreQueryOptions { +export interface FetchMoreQueryOptions { variables: Pick; } // Improved ObservableQuery field types, need to port them back to Apollo Client -type ObservableQueryFields = Pick< +export type ObservableQueryFields = Pick< ObservableQuery, 'startPolling' | 'stopPolling' > & { @@ -91,17 +91,13 @@ 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; loading: boolean; networkStatus: NetworkStatus; - fetchMore: ObservableQueryFields['fetchMore']; - refetch: ObservableQueryFields['refetch']; - startPolling: ObservableQueryFields['startPolling']; - stopPolling: ObservableQueryFields['stopPolling']; - updateQuery: ObservableQueryFields['updateQuery']; } export interface QueryProps { @@ -118,7 +114,7 @@ export interface QueryState { result: ApolloCurrentResult; } -interface QueryContext { +export interface QueryContext { client: ApolloClient; } diff --git a/src/Subscriptions.tsx b/src/Subscriptions.tsx index 17fcc99624..32dadfafe3 100644 --- a/src/Subscriptions.tsx +++ b/src/Subscriptions.tsx @@ -31,7 +31,7 @@ export interface SubscriptionState { error?: ApolloError; } -interface SubscriptionContext { +export interface SubscriptionContext { client: ApolloClient; } diff --git a/src/withApollo.tsx b/src/withApollo.tsx index b4795ab6ec..90d3bdc735 100644 --- a/src/withApollo.tsx +++ b/src/withApollo.tsx @@ -10,7 +10,7 @@ function getDisplayName

      (WrappedComponent: React.ComponentType

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

      = P & { client: ApolloClient }; +export type WithApolloClient

      = P & { client: ApolloClient }; export default function withApollo( WrappedComponent: React.ComponentType>, From c6a9cfe0b94e7e8c34b054aa749a43e8340bd504 Mon Sep 17 00:00:00 2001 From: Leonardo Andres Garcia Crespo Date: Thu, 1 Feb 2018 12:11:17 -0300 Subject: [PATCH 14/14] Increase max allowed bundle size --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c82d666b63..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": {