testing react component with 'relay-test-utils' - javascript

I am created some components with React with Relay/NextJs, I read the documentation for applied tests: https://relay.dev/docs/en/testing-relay-components, I have been tried to create the test for Fragment Container Tests, with the Mock Payload Generator and #relay_test_operation Directive:
import SearchPageContainer from '../components/SearchPageContainer'
import ReactTestRenderer from 'react-test-renderer'
import { QueryRenderer, graphql } from 'react-relay'
import {
createMockEnvironment,
MockPayloadGenerator,
generateAndCompile
} from 'relay-test-utils'
describe('SearchPageContainer', () => {
let testComponent
let environment
beforeEach(() => {
environment = createMockEnvironment()
const TestRenderer = () => (
<QueryRenderer
environment={environment}
query={graphql`
query TestQuery #relay_test_operation {
view: node(id: "test-id") {
...MyConnectionFragment
}
}
`}
variables={{}}
render={({ error, props }) => {
if (props) {
return <SearchPageContainer view={props} />
} else if (error) {
return error.message
}
return 'Loading...'
}}
/>
)
ReactTestRenderer.act(() => {
testComponent = ReactTestRenderer.create(<TestRenderer />)
})
})
it('should have pending operations in the queue', () => {
expect(environment.mock.getAllOperations().length).toEqual(1)
})
it('should resolve query', () => {
environment.mock.resolveMostRecentOperation(operation =>
MockPayloadGenerator.generate(operation)
)
expect(testComponent).toMatchSnapshot()
})
})
I have this error: Relay Transform Error: You supplied a field named node on type Query, but no such field exists on that type.
My configuration in the .babelrc is this:
{
"presets": [
"next/babel",
],
"plugins": [
["relay", {"compat": true, "schema": "schema/schema.json"}],
]
}
I don't know if I need some extra configuration for this #relay_test_operation directive works,
appreciate your help

There are two main modules that you will enjoy using in your tests:
createMockEnvironment
mockPayloadGenerator
The createMockEnvironment nothing more than an implementation the Relay Environment Interface and it also has an additional mock layer, with methods that allow resolving/reject and control of operations (queries/mutations/subscriptions). It is a special version of Relay Environment with additional API methods for controlling the resolving and rejection operations. Therefore the first thing we should do is tell jest that we don't want the default environment but our environment provided by relay-test-utils.
The mockPayloadGenerator is to improve the process of creating and maintaining the mock data for tested components it can generate dummy data for the selection that you have in your operation.
Check this post with examples: How to test your relay components with relay-test-utils and react-testing-library

Related

SolidJS: Updating component based on value served by Context's store?

In React all props are updated and propagated to children automatically which is nice but it slows down and requires lots of optimization at some point.
So I'm building an app with SolidJS using Context + createStore patterng and I'm having problems with consuming that state.
I'd like to create AppProvider component that manages State props and Dispatch functions. The Provider will be performing all operations on appStore, implement functions and serve them all via AppContextState and AppContextDispatch providers.
Then I need to consume that data to update components that are dependent on it reactively.
Look at the code below:
/// index.tsx
import { render } from 'solid-js/web';
import { AppProvider } from '#/providers/AppProvider';
import App from './App';
render(() => (
<AppProvider>
<App />
</AppProvider>
), document.getElementById('root') as HTMLElement);
/// AppProvider.tsx
import { createContext, useContext, JSX } from 'solid-js';
import { createStore } from 'solid-js/store';
// Interfaces
interface IAppState {
isConnected: boolean;
user: { name: string; }
}
interface IAppDispatch {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
}
// Initialize
const initialState = {
isConnected: false,
user: { name: '' }
}
const initialDispatch = {
connect: () => {},
disconnect: () => {}
}
// Contexts
const AppContextState = createContext<IAppState>();
const AppContextDispatch = createContext<IAppDispatch>();
export const useAppState = () => useContext(AppContextState);
export const useAppDispatch = () => useContext(AppContextDispatch);
// Provider
export const AppProvider = (props: { children: JSX.Element }) => {
const [appStore, setAppStore] = createStore<IAppState>(initialState);
async function connect() {
setAppStore("isConnected", true);
setAppStore("user", "name", 'Chad');
}
async function disconnect() {
setAppStore("isConnected", false);
setAppStore("user", "name", '');
}
return (
<AppContextState.Provider value={appStore}>
<AppContextDispatch.Provider value={{ connect, disconnect }}>
{props.children}
</AppContextDispatch.Provider>
</AppContextState.Provider>
)
}
/// App.tsx
import { useAppState, useAppDispatch } from '#/providers/AppProvider';
export default function App() {
const { user, isConnected } = useAppState();
const { connect, disconnect } = useAppDispatch();
return (
<Show when={isConnected} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
)
}
This component will show a button that should run the connect function and update isConnected state and make the component within <Show> block visible but it doesn't do anything.
I verified that state is being updated by logging data of appStore in connect method.
When I change the component to depend on user.name instead isConnected it works
<Show when={user.name} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
However my app has many components depending on various data types, including boolean that for some doesn't work in this example with SolidJS.
I'd like to know what am I doing wrong here and understand what is the best way to share state between components. I keep reading documentation and fiddling with it but this particular problem bothers me for a past few days.
Plain Values in Solid cannot be tracked
The problem here is that primitive values / variables cannot be reactive in solid. We have two ways of tracking value access: Through function calls, and through property getters/proxies (which use signals under the hood).
So, what happens when you access a store property?
const state = useAppState();
createEffect(() => {
console.log(state.isConnected)
})
In this case, the property access is occurring within the effect, so it gets tracked, and reruns when the property value updates. On the other hand, with this:
const { isConnected } = useAppState();
We are accessing the property at the top level of the component (which is untracked and not reactive in solid). So even though we use this value in a context that is reactive (like the when prop in `), we can't run any special under-the-hood tracking to set up updates.
So why did user.name work?
The reason is that stores are deeply reactive (for primitives, objects and arrays), so
const { user } = useAppState();
Means that you are eagerly accessing the user object (so if the user property changes, you won't get updated), but the properties of the user object were not accessed yet, they only get accessed further on, in <Show when={user.name}>, so the property access user.name is able to be tracked.

How to unit test a higher order component using jest and enzyme in react

I am currently trying to write a test to test what is inside of a higher order components
my test like so:
let Component = withEverything(Header);
let wrapper;
it('renders correctly', async () => {
wrapper = await mountWithSleep(
<Component componentProps={{ session: { id: '2' } }} />,
0.25
);
console.log(wrapper.debug());
});
});
outputs the following:
<Component>
<WithSession component={[Function: GlobalNav]} innerProps={{...}} />
</Component>
My with session file looks like the following:
import React, { Component, ComponentType } from 'react';
import { Session, session } from '#efa/web/src/modules/auth/authService';
import { Omit } from '#everlutionsk/helpers';
import { Subscription } from 'rxjs';
class WithSession extends Component<Props, State> {
state: State = {
session: undefined
};
private subscription: Subscription;
componentDidMount() {
this.subscription = session.subscribe(session => {
this.setState({ session });
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
if (this.state.session === undefined) return null;
const Component = this.props.component;
const props = { ...this.props.innerProps, session: this.state.session };
return <Component {...props} />;
}
}
/**
* Injects a current session to the given [component].
*/
export function withSession<P extends SessionProps>(
component: ComponentType<P>
): ComponentType<Omit<P, keyof SessionProps>> {
return props => <WithSession component={component} innerProps={props} />;
}
export interface SessionProps {
readonly session: Session | null;
}
interface Props {
readonly component: ComponentType;
readonly innerProps: any;
}
interface State {
readonly session: Session | null | undefined;
}
I have tried to do a jest.mock which gets me part of the way using this:
jest.mock('#efa/web/src/modules/auth/components/withSession', () => {
//#ts-ignore
const original = jest.requireActual(
'#efa/web/src/modules/auth/components/withSession'
);
return {
__esModule: true,
...original,
withSession: component => {
return component;
}
};
});
Using this module i can at least see a returned component instead of with session. But now the issue is i need to be able to set the session state. wondering if anyone can help?!
It is worth noting this is a project which we inherited i would not of implemented it this way ever!
You're correct that this type of code absolutely makes testing more difficult. The "best" way in this case its probably a bit of work because the nicest way to go about it would be to switch this thing to context; and then add options to your test framework mount method to populate that context how you want from individual tests. Though, you can reach something similar with old fashioned HOCs.
There's a cheaper way, and that would be to allow options to be passed to withEverything (if this is used by the app as well, you can create a mirror one called withTestHocs or similiar. I.e.
withTestHocs(Header, {
session: //... object here
})
Internally, this HOC would no longer call withSession whatsoever. Instead, it would call a HOC who's only purpose is to inject that session config object into the component for test reasons.
There's no reason to do complex mocking to get the session right on every test, its a waste of time. You only need that if you're actually testing withSession itself. Here you should be prioritising your test framework API that makes having custom session per test nice and simple. jest.mock is not easily parametrised, so that in itself is also another good reason to not go down that road. Again, the exception is when you're unit testing the actual session hoc but those tests are typically quite edge-casey and wont be using the core "test framework HOC" you'll use for all your userland/feature code -- which is what I'm focusing on here.
Note with this solution, you wouldn't need the complex jest mocking anymore (provided all tests were moved to the new way).
export const withEverything =
(Component, {session}) =>
({ providerProps, componentProps }) =>
(
<MockedProvider {...providerProps}>
<BrowserRouter>
<FlashMessage>
<Component {...componentProps} session={session} />
</FlashMessage>
</BrowserRouter>
</MockedProvider>
);
Now in your test
it('renders correctly', async () => {
wrapper = await mountWithSleep(
const withEverything(Header, { session: { id: '2' }}),
0.25
);
console.log(wrapper.debug());
});
If you need to be able to manipulate the session mid-test, you could do that by returning a method from withEverthing that allows the session to be set, but im not sure if you need it.

Load function from external script using #loadable/component in React

I have a JSON file with several filepaths to scripts that I want to be able to load dynamically into my React app, to build each component based on specifications that are in the metadata. Currently I have the metadata in my app as a Metadata data object.
metadata.json:
{
"component1": { "script": "./createFirstLayer.js" },
"component2": { "script": "./createSecondLayer.js" }
}
Each script exports a function that I want to be able to use to construct the component. For troubleshooting purposes, it currently only returns a simple message.
function createFirstLayer(name) {
return name + " loaded!";
}
export default createFirstLayer;
I did some research and identified the #loadable/component package. Using this package as import loadable from "#loadable/component";, I attempted to load my script into App.js like this:
async componentDidMount() {
Object.keys(Metadata).forEach(function(name) {
console.log(Metadata[name].script);
var createLayer = loadable(() => import(Metadata[name].script));
var message = createLayer(name);
console.log(message);
});
}
Everything I have tried throws the TypeError createLayer is not a function. How can I get the function loaded?
I have also attempted the lazy method.
I have recreated a working demo of my problem here.
EDIT: I have tried to put this at the top of my app
const scripts = {};
Object.keys(Metadata).forEach(async function(name) {
import(Metadata[name].script).then((cb) => scripts[name] = cb);
});
This causes the TypeError Unhandled Rejection (Error): Cannot find module './createFirstLayer.js'. (anonymous function)
src/components lazy /^.*$/ groupOptions: {} namespace object:66
I have also attempted
const scripts = {};
Object.keys(Metadata).forEach(async function(name) {
React.lazy(() => import(Metadata[name].script).then((cb) => scripts[name] = cb));
});
My goal is to be able to call the appropriate function to create particular layer, and match them up in the metadata.
You don't need #loadable/component for two reasons.
You can accomplish your goal with dynamic imports
'#loadable/component' returns a React Component object, not your function.
To use dynamic imports simply parse your JSON the way you were, but push the call to the import's default function into state. Then all you have to do is render the "layers" from within the state.
Like this:
import React, { Component } from "react";
import Metadata from "./metadata.json";
class App extends Component {
constructor(props) {
super(props);
this.state = { messages: [] };
}
async componentDidMount() {
Object.keys(Metadata).forEach(name=> import(`${Metadata[name].script}`).then(cb =>
this.setState((state, props) => ({ messages: [...state.messages, cb.default(cb.default.name)] }))));
}
render() {
return (
<div className="App">
{this.state.messages.map((m, idx) => (
<h1 key={idx}>{m}</h1>
))}
</div>
);
}
}
export default App;
Here is the working example

How to stub a module function with Cypress?

I want to create a test with Cypress that has a React component that uses an auth library (#okta/okta-react) with a HOC (withOktaAuth).
My component looks like this:
// Welcome.js
import { withOktaAuth } from '#okta/okta-react'
const Welcome = ({authState}) => {
return <div>{authState.isAuthenticated ? 'stubbed' : 'not working'}</div>
}
export default withOktaAuth(Welcome)
I tried to make a test like so:
// test.js
import * as OktaReact from '#okta/okta-react'
const withOktaAuthStub = Component => {
Component.defaultProps = {
...Component.defaultProps,
authState: {
isAuthenticated: true,
isPending: false
},
authService: {
accessToken: '123'
}
}
return Component
}
describe('Test auth', () => {
before(() => {
cy.stub(OktaReact, 'withOktaAuth').callsFake(withOktaAuthStub)
})
it('Stubs auth', () => {
cy.visit('/welcome')
cy.contains('stubbed')
})
})
When I run the test, the component still does not use the stubbed function. Any help is very much appreciated!
It's been 2 years that this questions was submitted, but for those who still encounters that error, Cypress provides a guide about Okta e2e testing: https://docs.cypress.io/guides/end-to-end-testing/okta-authentication#Adapting-the-back-end

Write a jest test to my first component

I just finished writing my first Reactjs component and I am ready to write some tests (I used material-ui's Table and Toggle).
I read about jest and enzyme but I feel that I am still missing something.
My component looks like this (simplified):
export default class MyComponent extends Component {
constructor() {
super()
this.state = {
data: []
}
// bind methods to this
}
componentDidMount() {
this.initializeData()
}
initializeData() {
// fetch data from server and setStates
}
foo() {
// manuipulatig data
}
render() {
reutrn (
<Toggle
id="my-toggle"
...
onToggle={this.foo}
>
</Toggle>
<MyTable
id="my-table"
data={this.state.data}
...
>
</MyTable>
)
}
}
Now for the test. I want to write a test for the following scenario:
Feed initializeData with mocked data.
Toggle my-toggle
Assert data has changed (Should I assert data itself or it is better practice to assert my-table instead?)
So I started in the very beginning with:
describe('myTestCase', () => {
it('myFirstTest', () => {
const wrapper = shallow(<MyComponent/>);
}
})
I ran it, but it failed: ReferenceError: fetch is not defined
My first question is then, how do I mock initializeData to overcome the need of calling the real code that using fetch?
I followed this answer: https://stackoverflow.com/a/48082419/2022010 and came up with the following:
describe('myTestCase', () => {
it('myFirstTest', () => {
const spy = jest.spyOn(MyComponent.prototype, 'initializeData'
const wrapper = mount(<MyComponent/>);
}
})
But I am still getting the same error (I also tried it with componentDidMount instead of initializeData but it ended up the same).
Update: I was wrong. I do get a fetch is not defined error but this time it is coming from the Table component (which is a wrap for material-ui's Table). Now that I come to think about it I do have a lot of "fetches" along the way... I wonder how to take care of them then.
fetch is supported in the browser, but jest/enzyme run in a Node environment, so fetch isn't a globally available function in your test code. There are a few ways you can get around this:
1: Globally mock fetch - this is probably the simplest solution, but maybe not the cleanest.
global.fetch = jest.fn().mockResolvedValue({
json: () => /*Fake test data*/
// or mock a response with `.text()` etc if that's what
// your initializeData function uses
});
2: Abstract your fetch call into a service layer and inject that as a dependency - This will make your code more flexible (more boilerplate though), since you can hide fetch implementation behind whatever interface you choose. Then at any point in the future, if you decide to use a different fetch library, you can swap out the implementation in your service layer.
// fetchService.js
export const fetchData = (url) => {
// Simplified example, only takes 'url', doesn't
// handle errors or other params.
return fetch(url).then(res => res.json());
}
// MyComponent.js
import {fetchService} from './fetchService.js'
class MyComponent extends React.Component {
static defaultProps = {
// Pass in the imported fetchService by default. This
// way you won't have to pass it in manually in production
// but you have the option to pass it in for your tests
fetchService
}
...
initializeData() {
// Use the fetchService from props
this.props.fetchService.fetchData('some/url').then(data => {
this.setState({ data });
})
}
}
// MyComponent.jest.js
it('myFirstTest', () => {
const fetchData = jest.fn().mockResolvedValue(/*Fake test data*/);
const fetchService = { fetchData };
const wrapper = mount(<MyComponent fetchService={fetchService} />);
return Promise.resolve().then(() = {
// The mock fetch will be resolved by this point, so you can make
// expectations about your component post-initialization here
})
}

Categories

Resources