App.tsx
import { StackNavigationProp } from '#react-navigation/stack';
...
export const App = () => {
const rootNavigation = useNavigation<StackNavigationProp<ParamList>>();
const { params } =
useRoute<RouteProp<ParamList, 'myScreen'>>();
const navigateToHomePage = (): void => {
if (params.isVerified) {
rootNavigation.reset({
index: 0,
routes: [
{
name: 'HomePage',
},
],
});
}
};
return (...)
}
App.test.tsx
import React from 'react';
import { render, fireEvent, waitFor, RenderAPI } from '#testing-library/react-native';
import { MigrationFlowContext } from 'src/contexts/migration/MigrationFlowContext';
import App from './App.tsx';
const mockedNavigate = jest.fn();
const mockedReset = jest.fn();
const mockSpy = jest.fn().mockImplementation(() => ({
params: {
isVerified: true,
},
}));
jest.mock('#react-navigation/native', () => {
const actualNav = jest.requireActual('#react-navigation/native');
return {
...actualNav,
useNavigation: () => ({
navigate: mockedNavigate,
reset: mockedReset,
}),
useRoute: async () => mockSpy(),
};
});
describe('App', () => {
beforeEach(() => {
mockSpy.mockReset();
});
describe('isVerified = true', () => {...})
describe('isVerified = false', () => {
mockSpy.mockImplementation(() => {
return {
params: {
isVerified: false,
},
};
});
test('Click Yes', async () => {
const { getByTestId } = renderHomeScreen();
...
});
test('Click No', () => {
const { getByTestId } = renderHomeScreen();
...
});
});
}
It didn't work with below error
TypeError: Cannot read properties of undefined (reading 'isVerified')
How to fix it?
Related
In the below slice code I am Getting the data from a server using createAsyncThunk. Now I am trying to delete data locally for which I have written a reducer called removeData.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import axios from "axios";
export const dataTableSlice = createSlice({
name: "dataTable",
initialState: {
isGridData: [],
isLoading: false,
},
reducers: {
removeData: (state, action) => {
const dataSource = [...state.isGridData];
const filteredData = dataSource.filter(
(item) => item.id !== action.payload.id
);
state.isGridData.push(filteredData);
},
},
extraReducers: (builder) => {
builder
.addCase(loadData.pending, (state, action) => {
state.isLoading = true;
})
.addCase(loadData.fulfilled, (state, action) => {
state.isGridData = [...action.payload.data];
state.isLoading = false;
});
},
});
export const loadData = createAsyncThunk("loadData", async () => {
return await axios.get("https://jsonplaceholder.typicode.com/comments");
});
export const { removeData } = dataTableSlice.actions;
export default dataTableSlice.reducer;
Component
import { Table,Popconfirm,Button } from 'antd';
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {loadData,removeData} from '../../features/DataTableState/DataTableSlice';
import "antd/dist/antd.css";
import './DataTable.scss';
const DataTable = () => {
const gridData = useSelector((state) => state.dataTable.isGridData);
const isLoading = useSelector((state) => state.dataTable.isLoading);
const dispatch = useDispatch();
useEffect(() => {
dispatch(loadData());
},[dispatch]);
const inventoryData = gridData.map((item) => ({
...item,
inventory:Math.floor(Math.random() * 6) + 20,
}));
const modifiedData = inventoryData.map(({body,...item}) =>({
...item,
key:item.id,
message:body,
}));
// const handleDelete = (record) =>{
// const dataSource = [...modifiedData];
// const filteredData = dataSource.filter((item) => item.id !== record.id);
// }
const columns = [
{
title:'Id',
dataIndex:'id',
align:'center',
},
{
title:'product',
dataIndex:'name',
align:'center',
editTable:true
},
{
title:'description',
dataIndex:'message',
align:'center',
editTable:true
},
{
title:'inventory',
dataIndex:'inventory',
align:'center',
editTable:false
},
{
title:'Action',
dataIndex:'action',
align:'center',
render: (_,record) =>
modifiedData.length >= 1 ? (
<Popconfirm title="Are you sure?" onConfirm={dispatch(removeData(record))}>
<Button danger type='primary'>Delete</Button>
</Popconfirm>
):null,
},
];
// const data = [
// {
// Id:1,
// product:'gas1',
// description:'18kg',
// inventory:52,
// },
// {
// Id:2,
// product:'gas1',
// description:'18kg',
// inventory:52,
// },
// {
// Id:3,
// product:'gas1',
// description:'18kg',
// inventory:52,
// },
// {
// Id:4,
// product:'gas1',
// description:'18kg',
// inventory:52,
// }
// ]
return (
<div className='data-table'>
<section className='space'></section>
<Table
className='table'
columns={columns}
dataSource={modifiedData}
bordered
loading={isLoading}
style={{flex:2}}/>
</div>
);
}
export default DataTable
Below given are errors in console:
1.serializableStateInvariantMiddleware.ts:195 A non-serializable value was detected in an action, in the path: payload.config.adapter. Value:
Take a look at the logic that dispatched this action: {type: 'loadData/fulfilled', payload: {…}, meta: {…}}
2.Warning: Cannot update a component (DataTable) while rendering a different component (Cell). To locate the bad setState() call inside Cell, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
I'm converting my app from JS to TS. Everything has been working good under JS but when started conversion to TS I'm getting plenty of errors with handle functions like for example handleVideoAdd. Does anyone has idea what am I'm doing wrong? Tried many things without success...
Property 'handleVideoAdd' does not exist on type 'undefined'. TS2339 - and it's pointing out to this fragment of code:
const { handleVideoAdd, inputURL, handleInputURLChange } = useContext(Context)
My code looks like that:
Header.tsx
import { Context } from "../Context";
import React, { useContext } from "react";
import { Navbar, Button, Form, FormControl } from "react-bootstrap";
export default function Header() {
const { handleVideoAdd, inputURL, handleInputURLChange } =
useContext(Context);
return (
<Navbar bg="light" expand="lg">
<Navbar.Brand href="#home">Video App</Navbar.Brand>
<Form onSubmit={handleVideoAdd} inline>
<FormControl
type="text"
name="url"
placeholder="Paste url"
value={inputURL}
onChange={handleInputURLChange}
className="mr-sm-2"
/>
<Button type="submit" variant="outline-success">
Add
</Button>
</Form>
</Navbar>
);
}
Context.tsx
import { useEffect, useMemo, useState } from "react";
import { youtubeApi } from "./APIs/youtubeAPI";
import { vimeoApi } from "./APIs/vimeoAPI";
import React from "react";
import type { FormEvent } from "react";
const Context = React.createContext(undefined);
function ContextProvider({ children }) {
const [inputURL, setInputURL] = useState("");
const [videoData, setVideoData] = useState(() => {
const videoData = localStorage.getItem("videoData");
if (videoData) {
return JSON.parse(videoData);
}
return [];
});
const [filterType, setFilterType] = useState("");
const [videoSources, setVideoSources] = useState([""]);
const [wasSortedBy, setWasSortedBy] = useState(false);
const [showVideoModal, setShowVideoModal] = useState(false);
const [modalData, setModalData] = useState({});
const [showWrongUrlModal, setShowWrongUrlModal] = useState(false);
const createModalSrc = (videoItem) => {
if (checkVideoSource(videoItem.id) === "youtube") {
setModalData({
src: `http://www.youtube.com/embed/${videoItem.id}`,
name: videoItem.name,
});
} else {
setModalData({
src: `https://player.vimeo.com/video/${videoItem.id}`,
name: videoItem.name,
});
}
};
const handleVideoModalShow = (videoID) => {
createModalSrc(videoID);
setShowVideoModal(true);
};
const handleVideoModalClose = () => setShowVideoModal(false);
const handleWrongUrlModalShow = () => setShowWrongUrlModal(true);
const handleWrongUrlModalClose = () => setShowWrongUrlModal(false);
const handleInputURLChange = (e) => {
setInputURL(e.currentTarget.value);
};
const handleVideoAdd = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const source = checkVideoSource(inputURL);
if (source === "youtube") {
handleYouTubeVideo(inputURL);
} else if (source === "vimeo") {
handleVimeoVideo(inputURL);
} else {
handleWrongUrlModalShow();
}
};
const checkVideoSource = (inputURL) => {
if (inputURL.includes("youtu") || inputURL.length === 11) {
return "youtube";
} else if (inputURL.includes("vimeo") || inputURL.length === 9) {
return "vimeo";
}
};
const checkURL = (inputURL) => {
if (!inputURL.includes("http")) {
const properURL = `https://${inputURL}`;
return properURL;
} else {
return inputURL;
}
};
const checkInputType = (inputURL) => {
if (!inputURL.includes("http") && inputURL.length === 11) {
return "id";
} else if (!inputURL.includes("http") && inputURL.length === 9) {
return "id";
} else {
return "url";
}
};
const fetchYouTubeData = async (videoID) => {
const data = await youtubeApi(videoID);
if (data.items.length === 0) {
handleWrongUrlModalShow();
} else {
setVideoData((state) => [
...state,
{
id: videoID,
key: `${videoID}${Math.random()}`,
name: data.items[0].snippet.title,
thumbnail: data.items[0].snippet.thumbnails.medium.url, //default, medium, high
viewCount: data.items[0].statistics.viewCount,
likeCount: data.items[0].statistics.likeCount,
savedDate: new Date(),
favourite: false,
source: "YouTube",
url: inputURL,
},
]);
setInputURL("");
}
};
const handleYouTubeVideo = (inputURL) => {
const inputType = checkInputType(inputURL);
if (inputType === "id") {
fetchYouTubeData(inputURL);
} else {
const checkedURL = checkURL(inputURL);
const url = new URL(checkedURL);
if (inputURL.includes("youtube.com")) {
const params = url.searchParams;
const videoID = params.get("v");
fetchYouTubeData(videoID);
} else {
const videoID = url.pathname.split("/");
fetchYouTubeData(videoID[1]);
}
}
};
const fetchVimeoData = async (videoID) => {
const data = await vimeoApi(videoID);
if (data.hasOwnProperty("error")) {
handleWrongUrlModalShow();
} else {
setVideoData((state) => [
...state,
{
id: videoID,
key: `${videoID}${Math.random()}`,
name: data.name,
thumbnail: data.pictures.sizes[2].link, //0-8
savedDate: new Date(),
viewCount: data.stats.plays,
likeCount: data.metadata.connections.likes.total,
savedDate: new Date(),
favourite: false,
source: "Vimeo",
url: inputURL,
},
]);
setInputURL("");
}
};
const handleVimeoVideo = (inputURL) => {
const inputType = checkInputType(inputURL);
if (inputType === "id") {
fetchVimeoData(inputURL);
} else {
const checkedURL = checkURL(inputURL);
const url = new URL(checkedURL);
const videoID = url.pathname.split("/");
fetchVimeoData(videoID[1]);
}
};
const deleteVideo = (key) => {
let newArray = [...videoData].filter((video) => video.key !== key);
setWasSortedBy(true);
setVideoData(newArray);
};
const deleteAllData = () => {
setVideoData([]);
};
const toggleFavourite = (key) => {
let newArray = [...videoData];
newArray.map((item) => {
if (item.key === key) {
item.favourite = !item.favourite;
}
});
setVideoData(newArray);
};
const handleFilterChange = (type) => {
setFilterType(type);
};
const sourceFiltering = useMemo(() => {
return filterType
? videoData.filter((item) => item.source === filterType)
: videoData;
}, [videoData, filterType]);
const sortDataBy = (sortBy) => {
if (wasSortedBy) {
const reversedArr = [...videoData].reverse();
setVideoData(reversedArr);
} else {
const sortedArr = [...videoData].sort((a, b) => b[sortBy] - a[sortBy]);
setWasSortedBy(true);
setVideoData(sortedArr);
}
};
const exportToJsonFile = () => {
let dataStr = JSON.stringify(videoData);
let dataUri =
"data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
let exportFileDefaultName = "videoData.json";
let linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute("download", exportFileDefaultName);
linkElement.click();
};
const handleJsonImport = (e) => {
e.preventDefault();
const fileReader = new FileReader();
fileReader.readAsText(e.target.files[0], "UTF-8");
fileReader.onload = (e) => {
const convertedData = JSON.parse(e.target.result);
setVideoData([...convertedData]);
};
};
useEffect(() => {
localStorage.setItem("videoData", JSON.stringify(videoData));
}, [videoData]);
return (
<Context.Provider
value={{
inputURL,
videoData: sourceFiltering,
handleInputURLChange,
handleVideoAdd,
deleteVideo,
toggleFavourite,
handleFilterChange,
videoSources,
sortDataBy,
deleteAllData,
exportToJsonFile,
handleJsonImport,
handleVideoModalClose,
handleVideoModalShow,
showVideoModal,
modalData,
showWrongUrlModal,
handleWrongUrlModalShow,
handleWrongUrlModalClose,
}}
>
{children}
</Context.Provider>
);
}
export { ContextProvider, Context };
App.js (not converted to TS yet)
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import "bootstrap/dist/css/bootstrap.min.css";
import reportWebVitals from "./reportWebVitals";
import { ContextProvider } from "./Context.tsx";
ReactDOM.render(
<React.StrictMode>
<ContextProvider>
<App />
</ContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();
This is because when you created your context you defaulted it to undefined.
This happens here: const Context = React.createContext(undefined)
You can't say undefined.handleVideoAdd. But you could theoretically say {}.handleVideoAdd.
So if you default your context to {} at the start like this: const Context = React.createContext({})
Your app shouldn't crash up front anymore.
EDIT: I see you're using TypeScript, in that case you're going to need to create an interface for your context. Something like this:
interface MyContext {
inputURL?: string,
videoData?: any,
handleInputURLChange?: () => void,
handleVideoAdd?: () => void,
deleteVideo?: () => void,
// and all the rest of your keys
}
Then when creating your context do this:
const Context = React.createContext<MyContext>(undefined);
I wrote a hook that calls apollo useQuery. It's pretty simple:
useDecider:
import { useState } from 'react';
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = name => {
const [enabled, setEnabled] = useState(false);
useQuery(GET_DECIDER, {
variables: {
name
},
onCompleted: data => {
const decision = data?.deciders[0]?.decision;
setEnabled(decision);
},
onError: error => {
return error;
}
});
return {
enabled
};
};
I'm trying to test it now and the MockedProvider is not returning the expected data:
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
const getMock = (value = false, decider = '') => [
{
request: {
query: GET_DECIDER,
variables: {
name: decider
}
},
result: () => {
console.log('APOLLO RESULT');
return {
data: {
deciders: [
{
decision: value,
name: decider,
value: 10
}
]
}
};
}
}
];
const FakeComponent = ({ decider }) => {
const { enabled } = useDecider(decider);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
const WrappedComponent = ({ decider, value }) => (
<MockedProvider mocks={getMock(value, decider)} addTypename={false}>
<FakeComponent decider={decider} />
</MockedProvider>
);
describe('useDecider', () => {
it('when decider returns true', () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = screen.getByText('isEnabled');
expect(result).toBeInTheDocument();
});
});
I simplified your hook implementation and put together a working example:
import { useQuery, gql } from "#apollo/client";
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { data } = useQuery(GET_DECIDER, { variables: { name } });
return { enabled: data?.deciders[0]?.decision || false };
};
Note that in the test I also updated your getBy to an await findBy:
describe("useDecider", () => {
it("when decider returns true", async () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = await screen.findByText("isEnabled");
expect(result).toBeInTheDocument();
});
});
This is because you need to wait for your API call to complete before the data will be on the page, hence you would not expect the data to be there on the first render.
From https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
To test how your component is rendered after its query completes, you
can await a zero-millisecond timeout before performing your checks.
This delays the checks until the next "tick" of the event loop, which
gives MockedProvider an opportunity to populate the mocked result
try adding before your expect call
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
I tried connecting my Todolist with MongoDB to store my data.
Since it is not recommended to fetch and update data within the reducer I'm a little confused on where to update the DB.
My thoughts are whenever I change the List with an Action, I would need to Replace the whole list on the DB, which doesnt seem efficient to me.
But when i only update only the changed element in the DB i dont see a reason to use useReducer.
Can someone help me how i should continue? :/
(This Todolist was using useStates and MongoDB before I tried exercising on useReducer, that's why the Routes and APIhelper include other functions)
App.js:
import React, { useState, useEffect, useReducer } from "react";
import APIHelper from "./APIHelper.js";
import Todo from "./components/Todo";
import "./index.css";
export const ACTIONS = {
ADD_TODO: "add-todo",
TOGGLE_TODO: "toggle-todo",
DELETE_TODO: "delete-todo",
SET_TODO: "set-todos",
};
const reducer = (state, action) => {
switch (action.type) {
case ACTIONS.SET_TODOS: {
return Object.assign({}, state.todos, {
todos: action.payload.todos,
});
}
case ACTIONS.ADD_TODO:
return [...state.todos, newTodo(action.payload.task)];
case ACTIONS.TOGGLE_TODO:
return state.todos.map((todo) => {
if (todo._id === action.payload.id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
case ACTIONS.DELETE_TODO:
return state.todos.filter((todo) => todo._id !== action.payload.id);
default:
return state.todos;
}
};
const newTodo = (task) => {
return { _id: Date.now(), task: task, completed: false };
};
export const setTodos = (todos) => {
return {
type: ACTIONS.SET_TODOS,
payload: {
todos,
},
};
};
const App = () => {
const initialState = {
todos: [],
};
const [state, dispatch] = useReducer(reducer, initialState);
const [task, setTask] = useState("");
useEffect(async () => {
const fetchTodoAndSetTodo = async () => {
const todos = await APIHelper.getAllTodos();
return todos;
};
const todos = await fetchTodoAndSetTodo();
//console.log(todos);
dispatch(setTodos(todos));
}, []);
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: ACTIONS.ADD_TODO, payload: { task: task } });
setTask("");
};
return (
<div>
{console.log(state.todos)}
<form onSubmit={handleSubmit}>
<input
type="text"
value={task}
onChange={(e) => setTask(e.target.value)}
/>
</form>
{state.todos &&
state.todos.map((todos) => {
return <Todo key={todos._id} todo={todos} dispatch={dispatch} />;
})}
{//APIHelper.updateTodo(state.todos)}
</div>
);
};
export default App;
Todo.js:
import React from "react";
import { ACTIONS } from "../App";
const Todo = ({ todo, dispatch }) => {
return (
<div>
<span style={{ color: todo.complete ? "#AAA" : "#000" }}>
{todo.task}
</span>
<button
onClick={() =>
dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })
}
>
Toggle
</button>
<button
onClick={() =>
dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })
}
>
Delete
</button>
</div>
);
};
export default Todo;
APIHelper.js:
import axios from "axios";
const API_URL = "http://localhost:8080/todos/";
const createTodo = async (task) => {
const { data: newTodo } = await axios.post(API_URL, {
task,
});
return newTodo;
};
const deleteTodo = async (id) => {
const message = await axios.delete(`${API_URL}${id}`);
return message;
};
const updateTodo = async (payload) => {
const { data: newTodo } = await axios.put(`${API_URL}`, payload);
return newTodo;
};
const getAllTodos = async () => {
const { data: todos } = await axios.get(API_URL);
return todos;
};
export default { createTodo, deleteTodo, updateTodo, getAllTodos };
routes.js:
const db = require("./db.js");
const routes = express.Router();
const success = (res, payload) => {
return res.status(200).json(payload);
};
routes.get("/", async (req, res, next) => {
try {
const todos = await db.Todo.find({}, "_id task completed");
return success(res, todos);
} catch (err) {
next({ status: 400, message: "failed to get todos" });
}
});
routes.post("/", async (req, res, next) => {
try {
const todo = await db.Todo.create(req.body);
return success(res, todo);
} catch (err) {
next({ status: 400, message: "failes to create todo" });
}
});
routes.put("/", async (req, res, next) => {
try {
const todo = await db.Todo.findByIdAndUpdate(req.params.id, req.body, {
new: true,
});
return success(res, todo);
} catch (err) {
next({ status: 400, message: "failed to update todo" });
}
});
routes.delete("/:id", async (req, res, next) => {
try {
await db.Todo.findByIdAndRemove(req.params.id);
return success(res, "todo deleted");
} catch (err) {
next({ status: 400, message: "failed to delete todo" });
}
});
routes.use((err, req, res, next) => {
return res.status(err.status || 400).json({
status: err.status || 400,
message: err.message || "there was an error processing request",
});
});
module.exports = routes;
```
So this is the component I am attempting to test:
export const Profile: FC<Props> = props => {
const { navigation, profileDetails, fetchProfileDetails } = props;
useEffect(() => {
!profileDetails && fetchProfileDetails();
}, []);
return (
<ScrollView>
{get(profileDetails, 'error') ? (
<Error message={get(profileDetails, 'message') || 'Unknown Error'} />
) : (
<UserContainer navigation={navigation} userDetails={profileDetails} />
)}
</ScrollView>
);
};
As you may see there are a couple of components, Error and UserContainer components.
This is my test:
import React from 'react';
import { dummyUserInfo } from '../../shared/models/UserInfo.model';
import { Profile } from './Profile';
import { shallowRender } from '../../shared/services/testHelper';
const mockNavigation: any = { getParam: () => undefined };
const mockError: any = { profileDetails: { error: 'test error' } };
describe('Profile Scene', () => {
it('renders error', () => {
const tree = shallowRender(<Profile profileDetails={mockError} navigation={mockNavigation} />);
expect(tree).toMatchSnapshot();
});
});
And this is how the snapshot for the renders error test looks:
exports[`Profile Scene renders error 1`] = `
<ScrollViewMock>
<UserContainer
navigation={
Object {
"getParam": [Function],
}
}
userDetails={
Object {
"profileDetails": Object {
"error": "test error",
},
}
}
/>
</ScrollViewMock>
`;
So is that a proper way to test that kind of things or is there a better way?
You should test for both scenarios separately.
One for the error container and one for User Container.
import React from 'react';
import { Profile } from './Profile';
import { shallowRender } from '../../shared/services/testHelper';
const mockNavigation: any = { getParam: () => undefined };
describe('Profile Scene', () => {
it('renders error', () => {
const mockError: any = { profileDetails: { error: 'test error', message: 'test error' } };
const tree = shallowRender(<Profile profileDetails={mockError} navigation={mockNavigation} />);
expect(wrapper.find(Error).exists()).to.equal(true);
expect(wrapper.find(UserContainer).exists()).to.equal(false);
});
it('renders naviagtion', () => {
const mockError: any = { profileDetails: { error: null, message: '' } };
const tree = shallowRender(<Profile profileDetails={mockError} navigation={mockNavigation} />);
expect(wrapper.find(Error).exists()).to.equal(false);
expect(wrapper.find(UserContainer).exists()).to.equal(true);
});
});