I'm following the docs provided by Apollo GraphQL for testing React component mutations using MockedProvider.
In the app I'm using recompose styled-components react apollo-client react-apollo
For testing I use jest and enzyme
I have created mock queries using MockedProvider and tested my component successfully. I cannot, however, get the mutation testing to work properly. My component keeps displaying an error state for the HTML instead of displaying an OK state.
NOTE: Through compose, I compose my <TestKey/> component with handler functions and a loading and error state that will render conditionally.
Following the docs (linked above) I've created a test that looks similar to this:
COMPONENT
const Key = ({
className,
columns,
title,
data,
type,
onClick,
paginate,
error,
info,
}) => (
<div className={className}>
<div className="tableHeader">
<h4>{title}</h4>
<Wrapper>
<Tooltip
title={`What are ${title}?`}
content={[<TooltipContent key={title}>{info}</TooltipContent>]}
trigger="hover"
keyid={title}
/>
</Wrapper>
<Button type="primary" onClick={() => onClick(type)}>
<ButtonIcon type="key" />
Generate New Key
</Button>
</div>
{error && <Alert type="danger">Error: {error}</Alert>}
{data.length === 0 ? (
<NoKeys>There are no {title} generated for this account.</NoKeys>
) : (
<Table
columns={columns}
data={data}
defaultSorted={[
{
id: 'insertedAt',
asc: false,
},
]}
/>
)}
</div>
)
MUTATION
const CREATEMUTATION = gql`
mutation createKey(
$type: String!
) {
createKey(
type: $type
) {
id
token
type
insertedAt
}
}
`
EXPECTED RETURN FROM MUTATION
const createKey = {
id: '4',
token: 'ucf345',
type: 'public',
insertedAt: '2018-06-20 20:42:15.189925',
}
TEST
describe('Create Key', () => {
let tree
beforeEach(async () => {
const mockedData = [{
request: {
query: CREATEMUTATION,
variables: { type: 'public' },
},
result: {
data: {
createKey: createKey,
},
},
}]
tree = renderer.create(
<MockedProvider mocks={mockedData} removeTypename={true}>
<ThemeProvider theme={theme}>
<TestKey />
</ThemeProvider>
</MockedProvider>
)
})
****at this step in the test I should be able to call on the button in the component and trigger a click that calls the mutation****
it('check', () => {
//errors because it cannot find button
const button = tree.root.findByProps({type: 'primary'})
console.log(tree.toJSON().children);
})
})
})
CONSOLE.LOG RETURNS THE FOLLOWING IN THE TERMINAL
(it displays what's rendered when my component receives an error from GraphQL)
[ { type: 'div',
props: { className: 'sc-bwzfXH kIOmnc' },
children:
[ [Object],
'Error connecting to the Platform. Wait for a second and then click the retry button below' ]
]
I've tried this using Enzyme's mount instead of React's Test renderer and I get the same result (an error state).
Related
I'm working on a project using react-query where I'm displaying a list. Each entry in the list consists of multiple input fields and it should be able to save just a single entry as well as possible to save all the entries at once.
While saving the data I want to display loading indicators for the elements that are being saved a retry button in case the saving fails and a success message when it works.
I see it's possible to get ahold of the MutationCache, but I can't seem to find anything about triggering mutations from outside the component where it's used.
I made a small codesandbox to illustrate the setup, otherwise my code is pasted below.
https://codesandbox.io/s/react-query-forked-5cuxgb?file=/src/Form.jsx
Form.js
import * as React from "react";
import { Person } from "./Person";
export const Form = () => {
const people = [
{
id: 1,
name: "John Doe",
age: 37
},
{
id: 2,
name: "Jack Johnson",
age: 45
},
{
id: 3,
name: "Jimmie Jones",
age: 23
}
];
const saveAll = () => {
// Trigger mutations here?
};
return (
<div>
{people.map((person) => (
<Person key={person.id} {...person} />
))}
<hr />
<button onClick={saveAll}>Save all</button>
</div>
);
};
Person.js
import * as React from "react";
import { useCreatePersonMutation } from "./useCreatePersonMutation";
export const Person = (props) => {
const { mutate, status } = useCreatePersonMutation(props.id);
return (
<div>
{status === "loading" && <span>Saving...</span>}
{status === "success" && <span>Success</span>}
{status === "error" && (
<button onClick={mutate} style={{ marginRight: 12 }}>
Retry
</button>
)}
{status === "idle" && (
<button onClick={mutate} style={{ marginRight: 12 }}>
Create Person
</button>
)}
<input value={props.name} disabled={status === "loading"} />
<input value={props.age} disabled={status === "loading"} />
</div>
);
};
useCreatePersonMutation
import { useMutation } from "react-query";
export const useCreatePersonMutation = (id) => {
return useMutation({
mutationKey: ["Create_Person", id],
mutationFn: () => new Promise((resolve) => setTimeout(resolve, 3000))
});
};
You can't really go into the mutation cache (queryClient.getMutationCache()) and look for existing mutations and invoke them, because mutations only "exist" once they have been invoked with .mutate or .mutateAsync.
So the mutations in your component aren't really "there yet".
The easiest solution would imo be to:
have a separate mutation that lives in the Form component
you invoke all requests in there in parallel to create all users
this will give you a separate loading state that you can either pass down to all components, or you just add one extra loading state (overlay) to the whole form while this mutation is running.
I ended up achieving the desired behaviour by executing a mutation for each person being saved.
const saveAll = () => {
Promise.allSettled(people.map((person) => mutateAsync(person)));
};
In the Person component that renders each row, I listen to the mutation cache and try to find the matching mutation by comparing the persons name with the name being passed to the mutation.
React.useEffect(() => {
queryClient.getMutationCache().subscribe((listener) => {
if (!listener) return;
if (listener.options?.variables.name !== name) return;
setStatus(listener.state.status);
});
}, [queryClient, name]);
This allows each Person component to show the status of the mutation. And retrying a mutation is as simple as executing the mutation.
const retry = () => {
const mutation = queryClient.getMutationCache().find({
predicate: (mutation) => mutation.options.variables.name === name
});
if (mutation) {
mutation.execute();
}
};
It doesn't scale well performance wise if you work with large lists, since each Person component gets notified about each and every mutation that gets triggered.
However the lists I work with are of limited size, so for now it seems to suit my needs.
https://codesandbox.io/s/react-query-forked-5cuxgb
I'm trying to make a dynamic form where the form input fields is rendered from data returned by an API.
Since atom needs to have a unique key, I tried wrapping it inside a function, but every time I update the field value or the component re-mounts (try changing tabs), I get a warning saying:
I made a small running example here https://codesandbox.io/s/zealous-night-e0h4jt?file=/src/App.tsx (same code as below):
import React, { useEffect, useState } from "react";
import { atom, RecoilRoot, useRecoilState } from "recoil";
import "./styles.css";
const textState = (key: string, defaultValue: string = "") =>
atom({
key,
default: defaultValue
});
const TextInput = ({ id, defaultValue }: any) => {
const [text, setText] = useRecoilState(textState(id, defaultValue));
const onChange = (event: any) => {
setText(event.target.value);
};
useEffect(() => {
return () => console.log("TextInput unmount");
}, []);
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
};
export default function App() {
const [tabIndex, setTabIndex] = useState(0);
// This would normally be a fetch request made by graphql or inside useEffect
const fields = [
{ id: "foo", type: "text", value: "bar" },
{ id: "hello", type: "text", value: "world" }
];
return (
<div className="App">
<RecoilRoot>
<form>
<button type="button" onClick={() => setTabIndex(0)}>
Tab 1
</button>
<button type="button" onClick={() => setTabIndex(1)}>
Tab 2
</button>
{tabIndex === 0 ? (
<div>
<h1>Fields</h1>
{fields.map((field) => {
if (field.type === "text") {
return (
<TextInput
key={field.id}
id={field.id}
defaultValue={field.value}
/>
);
}
})}
</div>
) : (
<div>
<h1>Tab 2</h1>Just checking if state is persisted when TextInput
is unmounted
</div>
)}
</form>
</RecoilRoot>
</div>
);
}
Is this even possible with recoil. I mean it seems to work but I can't ignore the warnings.
This answer shows how you can manually manage multiple instances of atoms using memoization.
However, if your defaultValue for each usage instance won't change, then Recoil already provides a utility which can take care of this creation and memoization for you: atomFamily. I'll quote some relevant info from the previous link (but read it all to understand fully):
... You could implement this yourself via a memoization pattern. But, Recoil provides this pattern for you with the atomFamily utility. An Atom Family represents a collection of atoms. When you call atomFamily it will return a function which provides the RecoilState atom based on the parameters you pass in.
The atomFamily essentially provides a map from the parameter to an atom. You only need to provide a single key for the atomFamily and it will generate a unique key for each underlying atom. These atom keys can be used for persistence, and so must be stable across application executions. The parameters may also be generated at different callsites and we want equivalent parameters to use the same underlying atom. Therefore, value-equality is used instead of reference-equality for atomFamily parameters. This imposes restrictions on the types which can be used for the parameter. atomFamily accepts primitive types, or arrays or objects which can contain arrays, objects, or primitive types.
Here's a working example showing how you can use your id and defaultValue (a unique combination of values as a tuple) as a parameter when using an instance of atomFamily state for each input:
TS Playground
body { font-family: sans-serif; }
input[type="text"] { font-size: 1rem; padding: 0.5rem; }
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/recoil#0.6.1/umd/recoil.min.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.7/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom';
// import type {ReactElement} from 'react';
// import {atomFamily, RecoilRoot, useRecoilState} from 'recoil';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {atomFamily, RecoilRoot, useRecoilState} = Recoil;
const textInputState = atomFamily<string, [id: string, defaultValue?: string]>({
key: 'textInput',
default: ([, defaultValue]) => defaultValue ?? '',
});
type TextInputProps = {
id: string;
defaultValue?: string;
};
function TextInput ({defaultValue = '', id}: TextInputProps): ReactElement {
const [value, setValue] = useRecoilState(textInputState([id, defaultValue]));
return (
<div>
<input
type="text"
onChange={ev => setValue(ev.target.value)}
placeholder={defaultValue}
{...{value}}
/>
</div>
);
}
function App (): ReactElement {
const fields = [
{ id: 'foo', type: 'text', value: 'bar' },
{ id: 'hello', type: 'text', value: 'world' },
];
return (
<RecoilRoot>
<h1>Custom defaults using atomFamily</h1>
{fields.map(({id, value: defaultValue}) => (
<TextInput key={id} {...{defaultValue, id}} />
))}
</RecoilRoot>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
I think the problem is from textState(id, defaultValue). Every time you trigger re-rendering for TextInput, that function will be called again to create a new atom with the same key.
To avoid that situation, you can create a global variable to track which atom added. For example
let atoms = {}
const textState = (key: string, defaultValue: string = "") => {
//if the current key is not added, should add a new atom to `atoms`
if(!atoms[key]) {
atoms[key] = atom({
key,
default: defaultValue
})
}
//reuse the existing atom which is added before with the same key
return atoms[key];
}
I'm trying to pass props from an onclick event to a function when then renders a component based on those prop values. The problem is that i am not able to pass on the props to the functional component. But it returns undefined
here is the parent component that passes the props using the map function
<DropdownButton id="dropdown-basic-button" title="Dropdown Menu">
{post && (postVideoInfo = post.video_info, postVideoInfo.map((postVideoSource) => (
<Dropdown.Item key={postVideoSource.id} onClick={RenderVideoPlayer} props={postVideoSource}>{postVideoSource.audio_language}</Dropdown.Item>
)))}
</DropdownButton>
here is the functional component that renders a new component on click
const RenderVideoPlayer = (props) => {
let videoSrc = {
type: 'video',
sources: [
{
src: `${props.video_url}`,
type: 'video/mp4',
}
],
poster: '/path/to/poster.jpg',
tracks: [
{
kind: 'captions',
label: `${props.caption_language}`,
srclang: 'en',
src: `${props.video_url}`,
default: true,
}
],
}
return (
<Plyr
options={options}
source={videoSrc}
/>
)
}
That data is retrieved from an api and stored in setState
It clearly seems like RenderVideoPlayer is not being passed any info from the postVideoSource in the map.
Rewriting the onClick as this could work, as you will be passing the props (I am guessing postVideSource has the 2 pieces of info that RenderVideoPlayer is expecting):
onClick={() => {
return <RenderVideoPlayer
video_url={postVideoSource.videoUrl}
caption_language={props.captionLanguate}
/>
}
I am also assuming this RenderVideoPlayer is going to be rendered inside a modal or some overlay or some side div...
I have a parent component, <AssetSelectorMenu>, which is composed of two child components:
export const AssetSelectorMenu = (({ assets, sortByName }) => {
return (
<>
<AssetSelectorHeader sortByName={sortByName} />
{assets && assets.map((asset) => (
<AssetSelectorRow key={asset} />
))}
</>
);
});
storybook for AssetSelectorMenu:
export const Default = () => (
<AssetSelectorMenu sortByName={action("sortByName")} assets={assets} />
);
Inside the storybook for AssetSelectorMenu, I'd like to test that the function prop sortByName actually visually sorts the assets by name. At the moment, it only makes sure it the function gets called, but visually it's not sorting the assets. How can I do that?
If you want to use state in your Storybook examples so that your components are fully working based on interaction you need to use the createElement function from React.
Here is a simple example using a Checkbox component that has it's value managed by state which simulates using a state manager like Redux or Context etc.
import { Fragment, useState, createElement } from 'react'
<Preview>
<Story name="Default">
{createElement(() => {
const [value, setValue] = useState(['Yes'])
const onChange = (event, value) => {
setValue(value)
}
return (
<Checkbox
name="checkbox"
values={[
{ label: 'Yes', value: 'Yes' },
{ label: 'No', value: 'No' }
]}
value={value}
onChange={onChange}
/>
)
})}
</Story>
</Preview>
The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};