@@ -2,6 +2,7 @@ import React, { Fragment, useEffect, useState } from 'react';
22import { DocumentNode , GraphQLError } from 'graphql' ;
33import gql from 'graphql-tag' ;
44import { act } from 'react-dom/test-utils' ;
5+ import userEvent from '@testing-library/user-event' ;
56import { render , screen , waitFor , renderHook } from '@testing-library/react' ;
67import {
78 ApolloClient ,
@@ -2399,6 +2400,229 @@ describe('useQuery Hook', () => {
23992400 } , { interval : 1 , timeout : 20 } ) ) . rejects . toThrow ( )
24002401 } ) ;
24012402
2403+ it ( 'should not return partial data from cache on refetch with errorPolicy: none (default) and notifyOnNetworkStatusChange: true' , async ( ) => {
2404+ const query = gql `
2405+ {
2406+ dogs {
2407+ id
2408+ breed
2409+ }
2410+ }
2411+ ` ;
2412+
2413+ const GET_DOG_DETAILS = gql `
2414+ query dog($breed: String!) {
2415+ dog(breed: $breed) {
2416+ id
2417+ unexisting
2418+ }
2419+ dogs {
2420+ id
2421+ breed
2422+ }
2423+ }
2424+ ` ;
2425+
2426+ const dogData = [
2427+ {
2428+ "id" : "Z1fdFgU" ,
2429+ "breed" : "affenpinscher" ,
2430+ "__typename" : "Dog"
2431+ } ,
2432+ {
2433+ "id" : "ZNDtCU" ,
2434+ "breed" : "airedale" ,
2435+ "__typename" : "Dog"
2436+ } ,
2437+ ] ;
2438+
2439+ const detailsMock = ( breed : string ) => ( {
2440+ request : { query : GET_DOG_DETAILS , variables : { breed } } ,
2441+ result : {
2442+ errors : [ new GraphQLError ( `Cannot query field "unexisting" on type "Dog".` ) ] ,
2443+ } ,
2444+ } ) ;
2445+
2446+ const mocks = [
2447+ {
2448+ request : { query } ,
2449+ result : { data : { dogs : dogData } } ,
2450+ } ,
2451+ // use the same mock for the initial query on select change
2452+ // and subsequent refetch() call
2453+ detailsMock ( 'airedale' ) ,
2454+ detailsMock ( 'airedale' ) ,
2455+ ] ;
2456+ const Dogs : React . FC < {
2457+ onDogSelected : ( event : React . ChangeEvent < HTMLSelectElement > ) => void ;
2458+ } > = ( { onDogSelected } ) => {
2459+ const { loading, error, data } = useQuery <
2460+ { dogs : { id : string ; breed : string ; } [ ] }
2461+ > ( query ) ;
2462+
2463+ if ( loading ) return < > Loading...</ > ;
2464+ if ( error ) return < > { `Error! ${ error . message } ` } </ > ;
2465+
2466+ return (
2467+ < select name = "dog" onChange = { onDogSelected } >
2468+ { data ?. dogs . map ( ( dog ) => (
2469+ < option key = { dog . id } value = { dog . breed } >
2470+ { dog . breed }
2471+ </ option >
2472+ ) ) }
2473+ </ select >
2474+ ) ;
2475+ } ;
2476+
2477+ const DogDetails : React . FC < {
2478+ breed : string ;
2479+ } > = ( { breed } ) => {
2480+ const { loading, error, data, refetch, networkStatus } = useQuery (
2481+ GET_DOG_DETAILS ,
2482+ {
2483+ variables : { breed } ,
2484+ notifyOnNetworkStatusChange : true
2485+ }
2486+ ) ;
2487+ if ( networkStatus === 4 ) return < p > Refetching!</ p > ;
2488+ if ( loading ) return < p > Loading!</ p > ;
2489+ return (
2490+ < div >
2491+ < div >
2492+ { data ? 'Partial data rendered' : null }
2493+ </ div >
2494+
2495+ < div >
2496+ { error ? (
2497+ `Error!: ${ error } `
2498+ ) : (
2499+ 'Rendering!'
2500+ ) }
2501+ </ div >
2502+ < button onClick = { ( ) => refetch ( ) } > Refetch!</ button >
2503+ </ div >
2504+ ) ;
2505+ } ;
2506+
2507+ const ParentComponent : React . FC = ( ) => {
2508+ const [ selectedDog , setSelectedDog ] = useState < null | string > ( null ) ;
2509+ function onDogSelected ( event : React . ChangeEvent < HTMLSelectElement > ) {
2510+ setSelectedDog ( event . target . value ) ;
2511+ }
2512+ return (
2513+ < MockedProvider mocks = { mocks } >
2514+ < div >
2515+ { selectedDog && < DogDetails breed = { selectedDog } /> }
2516+ < Dogs onDogSelected = { onDogSelected } />
2517+ </ div >
2518+ </ MockedProvider >
2519+ ) ;
2520+ } ;
2521+
2522+ render ( < ParentComponent /> ) ;
2523+
2524+ // on initial load, the list of dogs populates the dropdown
2525+ await screen . findByText ( 'affenpinscher' ) ;
2526+
2527+ // the user selects a different dog from the dropdown which
2528+ // fires the GET_DOG_DETAILS query, retuning an error
2529+ const user = userEvent . setup ( ) ;
2530+ await user . selectOptions (
2531+ screen . getByRole ( 'combobox' ) ,
2532+ screen . getByRole ( 'option' , { name : 'airedale' } )
2533+ ) ;
2534+
2535+ // With the default errorPolicy of 'none', the error is rendered
2536+ // and partial data is not
2537+ await screen . findByText ( 'Error!: ApolloError: Cannot query field "unexisting" on type "Dog".' )
2538+ expect ( screen . queryByText ( / p a r t i a l d a t a r e n d e r e d / i) ) . toBeNull ( ) ;
2539+
2540+ // When we call refetch...
2541+ await user . click ( screen . getByRole ( 'button' , { name : / R e f e t c h ! / i } ) )
2542+
2543+ // The error is still present, and partial data still not rendered
2544+ await screen . findByText ( 'Error!: ApolloError: Cannot query field "unexisting" on type "Dog".' )
2545+ expect ( screen . queryByText ( / p a r t i a l d a t a r e n d e r e d / i) ) . toBeNull ( ) ;
2546+ } ) ;
2547+
2548+ it ( 'should return partial data from cache on refetch' , async ( ) => {
2549+ const GET_DOG_DETAILS = gql `
2550+ query dog($breed: String!) {
2551+ dog(breed: $breed) {
2552+ id
2553+ }
2554+ }
2555+ ` ;
2556+ const detailsMock = ( breed : string ) => ( {
2557+ request : { query : GET_DOG_DETAILS , variables : { breed } } ,
2558+ result : {
2559+ data : {
2560+ dog : {
2561+ "id" : "ZNDtCU" ,
2562+ "__typename" : "Dog"
2563+ }
2564+ }
2565+ } ,
2566+ } ) ;
2567+
2568+ const mocks = [
2569+ // use the same mock for the initial query on select change
2570+ // and subsequent refetch() call
2571+ detailsMock ( 'airedale' ) ,
2572+ detailsMock ( 'airedale' ) ,
2573+ ] ;
2574+
2575+ const DogDetails : React . FC < {
2576+ breed ?: string ;
2577+ } > = ( { breed = "airedale" } ) => {
2578+ const { data, refetch, networkStatus } = useQuery (
2579+ GET_DOG_DETAILS ,
2580+ {
2581+ variables : { breed } ,
2582+ notifyOnNetworkStatusChange : true
2583+ }
2584+ ) ;
2585+ if ( networkStatus === 1 ) return < p > Loading!</ p > ;
2586+ return (
2587+ // Render existing results, but dim the UI until the results
2588+ // have finished loading...
2589+ < div style = { { opacity : networkStatus === 4 ? 0.5 : 1 } } >
2590+ < div >
2591+ { data ? 'Data rendered' : null }
2592+ </ div >
2593+ < button onClick = { ( ) => refetch ( ) } > Refetch!</ button >
2594+ </ div >
2595+ ) ;
2596+ } ;
2597+
2598+ const ParentComponent : React . FC = ( ) => {
2599+ return (
2600+ < MockedProvider mocks = { mocks } >
2601+ < DogDetails />
2602+ </ MockedProvider >
2603+ ) ;
2604+ } ;
2605+
2606+ render ( < ParentComponent /> ) ;
2607+
2608+ const user = userEvent . setup ( ) ;
2609+
2610+ await waitFor ( ( ) => {
2611+ expect ( screen . getByText ( 'Loading!' ) ) . toBeTruthy ( ) ;
2612+ } , { interval : 1 } ) ;
2613+
2614+ await waitFor ( ( ) => {
2615+ expect ( screen . getByText ( 'Data rendered' ) ) . toBeTruthy ( ) ;
2616+ } , { interval : 1 } ) ;
2617+
2618+ // When we call refetch...
2619+ await user . click ( screen . getByRole ( 'button' , { name : / R e f e t c h ! / i } ) )
2620+
2621+ // Data from the cache remains onscreen while network request
2622+ // is made
2623+ expect ( screen . getByText ( 'Data rendered' ) ) . toBeTruthy ( ) ;
2624+ } ) ;
2625+
24022626 it ( 'should persist errors on re-render with inline onError/onCompleted callbacks' , async ( ) => {
24032627 const query = gql `{ hello }` ;
24042628 const mocks = [
0 commit comments