Mock navigation service in jest - javascript

I'm using react-navigation and calling that service in my code. Although I'm not sure how to mock the navigate function. Here is my code:
import { NavigationActions } from 'react-navigation';
let _navigator;
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
Here is what I got so far:
export const loginEpic = (action$, state$, { ajax, navigate }) =>
action$.pipe(
ofType(LOGIN_REQUEST),
map(() => state$.value),
switchMap((options) =>
ajax(options).pipe(
pluck("response"),
map((res) => loginSuccess(res)),
tap((r) => navigate(ROUTES.DASHBOARD_SCREEN))
)
)
);
navigate is navigationService.navigate and I'm passing it from the dependencies of redux-observables.
The test looks like this:
const dependencies = {
ajax: ({ }) => of(mockedResponseFromAjax),
navigate: () => // ???
};
const result$ = loginEpic(action$, state$, dependencies).pipe(toArray());

Option 1: - What worked for me:
import NavigationService from '<your-path>/NavigationService';
beforeEach(() => {
NavigationService.navigate = jest.fn();
});
within my test file.
Option 2: - Better Version
see https://stackoverflow.com/questions/55319581/why-isn´t-mock-from-mock-folder-invoked-when-running-test-for-redux-action
it is important that you activate the mock by jest.mock('../../app/utils/NavigationService'); this has to be placed at the top of your test file directly behind the imports

Related

Conflicts between queryClient.clear() and mock the useParams()

When I add this line to the test setup, the mocking of useParams stops to work. I cannot find why it happens.
testsetup.ts
import '#testing-library/jest-dom'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3
}
}
})
// Clean up the react queries cache for each test.
afterEach(() => queryClient.clear())
component.test.tsx
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom') as any),
useParams: () => ({
tenantAlias: 'DE'
})
}))
test('Showing a navigation', async () => {
... some tests
})
Tested component is using useParams() to get params['tenantAlias'], but with queryClient.clear() it stops to returned mocked data, otherwise it works correct.
Why??? :)

Mock different results of an api called inside react component in jest

I have a react component that calls an API that returns two different results which the default value is
{init:false}
And based on users actions, it will be true:
{init:true}
Now I want to test these two states in my app.test.tsx, It will work when I skip one of them(each working fine without another one):
import { screen } from '#testing-library/react';
import { render } from 'src/mocks/renderViaAllProviders';
import App from './app';
import * as apis from 'src/api/consul';
import { mockedRawConsul } from 'src/mocks/db/consul';
test("Show init page when 'initialized:false' in consul.", async () => {
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
});
test("Show Login page when 'initialized:true' in consul", async () => {
const initializedConsul = {
...mockedRawConsul,
...{ configs: { initialized: true } }
};
/*eslint-disable */
//#ts-ignore
apis.getConsulPublicConfig = jest.fn(() =>
Promise.resolve(initializedConsul)
);
render(<App />);
const loginButton = await screen.findByRole('button', {
name: /regularLogin/i
});
expect(loginButton).toBeInTheDocument();
});
How can I fix this?
Update
Here is the reprex and the error :
● Show Login page when 'initialized:true' in consul
Unable to find role="textbox"
console.error
TypeError: Cannot read property 'status' of undefined
at onResponseRejected (\src\api\
service\interceptors.ts:18:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at getLicense (\src\api\license .
ts:10:20)
I have tried to simulate the example that you are trying, I am able to mock the API which returns different results and test for the same, but since we want different results when a Component is rendered the API will be called only once(assuming the API is called on mounting) and upon some user actions the Component will be mounted again that's why called render function again not sure whether it is a good practice or not
//App.js
export default function App() {
const [value, setValue] = useState('loading');
const [show, setShow] = useState({init: false})
useEffect(() => {
setTimeout(() => {
setValue('init page')
fetchData().then(data => {
setShow(data)
}).catch((error) => {
console.log(`ERROR`)
})
},0)
},[])
const { init = false} = show
return (
<>
<p>IT S APP</p>
<h1>Value is {value}</h1>
{ init ? <button>regular Login</button> : null}
</>
);
}
//api.js
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/posts").then((response) =>
Promise.resolve({init: true})
);
}
export { fetchData };
//App.test.js
import App from "./App";
import { fetchData }from './api';
jest.mock('./api')
describe("<App />", () => {
it("check if loading, login button is present",async () => {
fetchData.mockImplementationOnce(() => Promise.resolve({init: false}))
fetchData.mockImplementationOnce(() => Promise.resolve({init: true}))
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
render(<App />);
await waitFor(() => {
expect(screen.queryByRole('button', {
name: /regular Login/i
})).toBeInTheDocument();
})
});
});

Nextjs: How to register the quill-blot-formatter to dynamically imported react-quill on client side rendering only?

I am importing react-quill dynamically on the client side only using ssr: false. My functional component which is working fine, I want to add the quill-blot-formatter package to the modules part of my quill component.
My first roadblock is, I can't register this quill-blot-formatter with Quill as it shows:
ServerError
ReferenceError: document is not defined
This page is client rendered, therefore I don't understand where this error is coming from!
This is my code:
import dynamic from "next/dynamic";
import BlotFormatter from "quill-blot-formatter";
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
ssr: false,
loading: () => <p>Loading...</p>,
})
Quill.register("modules/blotFormatter", BlotFormatter);
Here, I don't understand how to bring Quill out of react-quill now that it's being imported dynamically. Therefore, I think that Quill.register isn't working. Now, how do I register quill-blot-formatter with react-quill? Following the Next.js example with react-quill, I am not even importing react-quill as ReactQuill as is the default export in the package.
Then I declared the blotFormatter module like this.
const modules = {
blotFormatter: {}, // here
toolbar: [
[{header: '1'}, {header: '2'}, {font: []}],
[{size: []}],
...
],
}
const formats = ['header','font','size','bold','italic','underline',...]
And used in the render() method like this:
export default function NewContent() {
...
render(
<QuillNoSSRWrapper
className={styles.quillTextArea}
id="quilleditor"
modules={modules}
formats={formats}
theme="snow"
onChange={handleTextChange}
readOnly={false}
/>
);
}
So far, this QuillNoSSRWrapper child component is doing it's job fine, but, how do I use the quill-blot-formatter in it's formats?
UPDATE
You don't need another useEffect to register module you can do it when you importing ReactQuill.
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
const { default: BlotFormatter } = await import("quill-blot-formatter");
RQ.Quill.register("modules/blotFormatter", BlotFormatter);
return function forwardRef({ forwardedRef, ...props }) {
return <RQ ref={forwardedRef} {...props} />;
};
},
{
ssr: false,
}
);
With this code you importing ReactQuill, register you module and pass ref that you can use later see details below. So with this code you now don't need any state. Additionally you can add custom loading function to dynamic details here.
After one day of searching i found the solution. First of all you import dynamic ReactQuill.
import dynamic from 'react/dynamic'
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
Or if you want to pass ref like this
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
// eslint-disable-next-line react/display-name
return ({ forwardedRef, ...props }) => <RQ ref={forwardedRef} {...props} />;
},
{
ssr: false,
}
);
And then you can use ref like this <ReactQuill ... forwardedRef={quillRef}/>
So then after you imported ReactQuill on client side you need to register module i found this solution here it's looks strange and i had no time to improve it but it's work. Here the code.
const loadQuill = async () => {
return new Promise(async (resolve, reject) => {
const Quill = await require("react-quill").Quill;
const BlotFormatter = (await import("quill-blot-formatter")).default;
resolve({ Quill, BlotFormatter });
})
.then(({ Quill, BlotFormatter }) => {
Quill.register("modules/blotFormatter", BlotFormatter);
return;
})
.then((value) => {
setEnableEditor(true);
});
};
useEffect(() => {
loadQuill();
}, []);
This code will execute on client side end register module. Also as you can see you need to declarate state enableEditor. And render ReactQuill only when enableEditor is true
{enableEditor && <ReactQuill ... />}
Looks bit wierd so maybe I will update it later

Accessing multiple easy-peasy stores from single component

I'm trying to access 2 different stores in a single component, but worry that perhaps the architecture of my app may need to change as easy-peasy may not have this functionality.
I have a GlobalStore
import { createStore } from 'easy-peasy';
const globalModel = {
menuOpen: false,
toggleMenu: action((state, payload) => {
state.menuOpen = payload;
}),
};
const GlobalStore = createStore(globalModel);
export default GlobalStore;
Just for this example, I'll use a single state and action used in the store to define whether the navigation menu is open or not.
The GlobalStore appears at the top level of my app in my App.js file.
import React from 'react';
import { StoreProvider } from 'easy-peasy';
import GlobalStore from './store/GlobalStore';
const App = () => {
return (
<StoreProvider store={GlobalStore}>
</StoreProvider>
);
};
export default App;
Now, further down the tree, I have another store SearchStore that dictates which view is active in the component.
import { createStore } from 'easy-peasy';
import { action } from 'easy-peasy';
const searchModel = {
view: 'filter',
setView: action((state, payload) => {
state.view = payload;
}),
};
const SearchStore = createStore(searchModel);
export default SearchStore;
The issue I have now is that in a component that I need to be able to access both stores to update the view with the setView action in the SearchStore and get the value of menuOpen from the GlobalStore but cannot access both concurrently.
The example I have in a component is that I have a styled component that when clicked calls the action setView but its position is also defined by whether the menuOpen is true or not. but obviously, if I try and get the state of menuOpen it will be undefined as it does not exist in SearchStore
const Close = styled.span`
$(({ menuOpen }) => menuOpen ? `
// styles go here
` : `` }
`;
const setView = useStoreActions((action) => action.setView);
const menuOpen = useStoreState((state) => state.menuOpen);
<Close menuOpen={menuOpen} onClick={() => setView('list')}>
Is this possible? Any help would be much appreciated.
Alternative 1: extending the global store
To access both store (via the useStoreState/Actions from the StoreProvider), you could nest both "sub" stores into the GlobalStore:
// SearchModel.js
import { action } from 'easy-peasy';
const searchModel = {
view: 'filter',
setView: action((state, payload) => {
state.view = payload;
}),
};
export default searchModel;
// MenuModel.js
import { action } from 'easy-peasy';
const menuModel = {
isOpen: false,
toggle: action((state, payload) => {
state.isOpen = !state.isOpen;
}),
};
export default menuModel;
// GlobalStore.js
import { createStore } from 'easy-peasy';
import menu from './MenuhModel';
import search from './SearchModel';
const globalModel = {
menu,
search,
};
const GlobalStore = createStore(globalModel);
export default GlobalStore;
This way, you can access both stores at your convenience, using the hooks:
const searchState = useStoreState((state) => state.search);
const menuState = useStoreState((state) => state.menu);
const searchActions = useStoreActions((action) => action.search);
const menuActions = useStoreActions((action) => action.menu);
Alternative 2: useLocalStore()
If you do not want to extend the global store, you could create a local store, by using the useLocalStore():
function Menu() {
const [state, actions] = useLocalStore(() => ({
isOpen: false,
toggle: action((state, payload) => {
state.isOpen = !state.isOpen;
}),
}));
return (
<div>
{state.isOpen && <MenuItems />}
<button onClick={() => actions.toggle()}>Open menu</button>
</div>
);
}
However, the drawback of this approach, is that the state is not global and only available at the component-level.
You could however get around this, by creating your own provider - but then again, alternative 1 would probably be the path of least resistance.

React-native and redux actions

SomeAction is not a function. ( In 'SomeAction()', 'SomeAction' is undefined ).
I get this error when i execute the SomeAction function.
If i only have SomeAction in my actions file and i do
export default SomeAction;
and then import it as below
import SomeAction from 'path/to/action'
it works fine. But since i want more than one functions, i did the following.
this is my Actions.js
const SomeAction = () => dipatch => ({
// Code here
});
const AnotherAction = () => dispatch => ({
// Code here
});
export default { SomeAction, AnotherAction };
then in my App.js
import { SomeAction } from 'path/to/action';
// Here the eslint gives me an error -> 'SomeAction not found in "path/to/action"'
const App = ({ SomeAction }) => {
// Code here
};
App.propTypes = {
SomeAction: PropTypes.func,
}
const mapStateToProps = state => ({
error: state.user.error,
});
export default connect(
mapStateToProps,
{ SomeAction }
)(App);
This worked on a React web app i was coding. Why not in React-Native?
Reviewing, I see you are exporting by default two methods. Normal implementation is one method. Another solution to do this is exporting one by one methods and importing them with their names.
Example of Exporting:
export const SomeAction = () => dipatch => ({
// Code here
});
export const AnotherAction = () => dispatch => ({
// Code here
});
Example of Importing:
import { SomeAction, AnotherAction } from 'path/to/action';
This example is a normal way to export and import functions.
You can not have two default methods exported.
export const SomeAction = () => dipatch => ({
// Code here
};
export const AnotherAction = () => dispatch => ({
// Code here
};
they will be available in your App component in as follow:
import { SomeAction , AnotherAction} from 'path/to/action';
Importing a Defautl export is as follow
import { SomeAction } from 'path/to/action'; or import SomeAction from 'path/to/action';
just use this abpve export const way

Categories