How to test a presentation component using React Testing Library - javascript

I have a Tabs component here.
Tabs.jsx
import React from "react";
import { Radio } from "antd";
function Tabs({ modes, selectedMode, handleChange }) {
return (
<Radio.Group onChange={handleChange} value={selectedMode}>
{modes.map((opt) => (
<Radio.Button key={opt.id} value={opt.id}>
{opt.label}
</Radio.Button>
))}
</Radio.Group>
);
}
export default Tabs;
App.jsx
import "./styles.css";
import Tabs from "./Tabs";
import React, { useState } from "react";
import "antd/dist/antd.css";
const MODES = [
{ id: 1, label: "Label 1" },
{ id: 2, label: "Label 2" }
];
export default function App() {
const [mode, setMode] = useState(MODES[0]);
const handleModeChange = (e) => {
setMode(e.target.value);
};
return (
<Tabs modes={MODES} selectedMode={mode} handleChange={handleModeChange} />
);
}
Here's the Codesandbox
https://codesandbox.io/s/vigilant-tesla-tcj4p8
Currently I try to test for the Tabs component using Jest and React Testing Library.
There are 2 ways I can think of:
Test the styles change after user interaction
describe('Tabs', () => {
it('works correctly', () => {
const MOCK_MODES = [
{ id: 1, label: "Label 1" },
{ id: 2, label: "Label 2" }
];
const [mockSelected, setMockSelected] = useState(MOCK_MODES[0].id)
const mockHandleModeChange = (e) => {
setMode(e.target.value);
};
const { container, getByText } = render(<Tabs modes={MOCK_MODES} handleChange={mockHandleModeChange}/>);
expect(container.firstChild).toHaveClass('ant-radio-button-wrapper-checked')
expect(container.secondChild).not.toHaveClass('ant-radio-button-wrapper-checked')
const item = getByText('Label 2');
fireEvent.click(item);
expect(container.firstChild).not.toHaveClass('ant-radio-button-wrapper-checked')
expect(container.secondChild).toHaveClass('ant-radio-button-wrapper-checked')
});
});
Just check whether the handleChange event is fired, with black-box testing.
describe('Tabs', () => {
it('works correctly', () => {
const MOCK_MODES = [
{ id: 1, label: "Label 1" },
{ id: 2, label: "Label 2" }
];
const mockSelected = 1;
const mockHandleModeChange = (e) => jest.fn();
const { getByText } = render(<Tabs modes={MOCK_MODES} handleChange={mockHandleModeChange}/>);
const item = getByText('Label 2');
fireEvent.click(item);
expect(mockHandleModeChange).toHaveBeenCalledTimes(1);
});
});
Which one should be the best? Or any better suggestions?

Related

How do I call a function which is inside a function component in React js 18?

I'm using a npm package called ReactDataGrid which has SelectEditor module which renders a combo box. In the editorProps, I am able to set a function which needs to be called on onChange event. This function setClientonChange needs to call another function which is nested inside a function component? How can I call it?
import React, {useState } from 'react';
import ReactDataGrid from '#inovua/reactdatagrid-community';
const columns = [
...
{ name: 'currency_id', groupBy: false, defaultFlex: 1, maxWidth: 150, textAlign: 'center', header: 'Currency', editor: SelectEditor, editable:true,
editorProps: {
dataSource: ['Dollar', 'Euro', 'Pound', 'INR'].map((element) => ({
id: element,
label: element
})),
setClientonChange(){
//have to call setCurrencyValue() here
}
}
}
];
const RoomDeposit = () => {
const [gridRef, setGridRef] = useState(null);
const setCurrencyValue = () => {
gridRef.current.setItemPropertyAt(2, 'amount', '20')
}
return (
<ReactDataGrid
onReady={setGridRef}
columns={columns}
dataSource={dataSource}
/>
);
}
export default () => <RoomDeposit />
import React, { useState } from "react";
import ReactDataGrid from "#inovua/reactdatagrid-community";
const columns = (setCurrencyValue) => [
...{
name: "currency_id",
groupBy: false,
defaultFlex: 1,
maxWidth: 150,
textAlign: "center",
header: "Currency",
editor: SelectEditor,
editable: true,
editorProps: {
dataSource: ["Dollar", "Euro", "Pound", "INR"].map((element) => ({
id: element,
label: element,
})),
setClientonChange() {
//have to call setCurrencyValue() here
setCurrencyValue();
},
},
},
];
const RoomDeposit = () => {
const [gridRef, setGridRef] = useState(null);
const setCurrencyValue = () => {
gridRef.current.setItemPropertyAt(2, "amount", "20");
};
return (
<ReactDataGrid
onReady={setGridRef}
columns={columns(setCurrencyValue)}
dataSource={dataSource}
/>
);
};
export default () => <RoomDeposit />;
Really I am not able to understand the question self.
we can execute global function from inside of function.
Function should be declarer first then execute.
But as per my understanding the answer can be look like this.
import React, {useState } from 'react';
import ReactDataGrid from '#inovua/reactdatagrid-community';
const RoomDeposit = () => {
const [gridRef, setGridRef] = useState(null);
const setCurrencyValue = () => {
gridRef.current.setItemPropertyAt(2, 'amount', '20')
}
function onDropdownChange(){
setCurrencyValue(); //You can execute from here
}
return (
<ReactDataGrid
onReady={setGridRef}
columns={columns}
dataSource={dataSource}
/>
);
}
export default () => <RoomDeposit />

tinymce react plugin registration does not work

Was trying to register a custom plugin in tinymce but ever attempt failed.
Created a plugin using yoman generator build it and reference in the project
Created a plugin using yoman generator directly reference in the project without building
Also tried the below approach is still doesn't work
import { FC, RefObject, useLayoutEffect, useRef } from 'react'
import { Editor } from '#tinymce/tinymce-react';
import { plugins, toolbar, quickBarsInsertToolbar } from './config';
import tinymce, { Editor as TinyMCEEditor, PluginManager } from 'tinymce';
import './index.css'
// import '../../plugins/dropdown-plugin/dist/dropdown-plugin/plugin.min.js';
const TinymceEditor: FC = () => {
const editorRef = useRef<TinyMCEEditor | null>(null);
const log = () => {
if (editorRef.current) {
console.log(editorRef.current.getContent());
}
};
useLayoutEffect(() => {
tinymce.PluginManager.add("dropdown-plugin", function (n, t) { n.ui.registry.addButton("dropdown-plugin", { text: "dropdown-plugin button", onAction: function () { n.setContent("<p>content added from dropdown-plugin</p>") } }) })
}, [])
return (
<div
className='tinymce_editor'
>
<Editor
onInit={(evt, editor) => editorRef.current = editor}
init={{
height: 500,
plugins:['dropdown-plugin'],
toolbar:'dropdown-plugin'
}}
/>
<button onClick={log}>Log editor content</button>
</div>
)
}
export default TinymceEditor;
Gives this error
Finally Working now
import { FC, useLayoutEffect, useRef } from 'react'
import { Editor } from '#tinymce/tinymce-react';
import { plugins, toolbar, quickBarsInsertToolbar } from './config';
import { Editor as TinyMCEEditor } from 'tinymce';
import './index.css'
import { registerPlugins } from './plugin/register-plugins';
const TinymceEditor: FC = () => {
const editorRef = useRef<TinyMCEEditor | null>(null);
const log = () => {
if (editorRef.current) {
console.log(editorRef.current.getContent());
}
};
useLayoutEffect(() => {
registerPlugins()
}, [])
return (
<div
className='tinymce_editor'
>
<Editor
onInit={(evt, editor) => editorRef.current = editor}
init={{
height: 500,
plugins: plugins,
quickbars_insert_toolbar: quickBarsInsertToolbar,
toolbar: toolbar
}}
/>
<button onClick={log}>Log editor content</button>
</div>
)
}
export default TinymceEditor;
import { customQuickBarButton } from './customQuickBarButton';
import tinymce from 'tinymce';
import { customToolBarButtonMenu } from './customToolBarButtonMenu';
export const registerPlugins = () => {
tinymce.PluginManager.add(
'custom-quickbar-button-plugin',
customQuickBarButton
);
tinymce.PluginManager.add(
'custom-toolbar-button-menu-plugin',
customToolBarButtonMenu
);
};
import ReactDOM from 'react-dom';
import { Editor } from 'tinymce';
export const customToolBarButtonMenu = (editor: Editor) => {
editor.ui.registry.addMenuButton('custom-toolbar-button-menu-plugin', {
text: 'Custom Menu',
icon: 'language',
fetch: callback => {
var items = [
{
type: 'menuitem',
text: 'Menu Item 1',
icon: 'arrow-right',
onAction: () => {
editor.insertContent(' <em>You clicked menu item 1!</em> ');
},
},
{
type: 'menuitem',
text: 'Menu Item 2',
icon: 'user',
onAction: () => {
const div = document.createElement('div');
ReactDOM.render(
<div>
<span>Signature with image</span>
<img src="https://i.picsum.photos/id/532/200/200.jpg?hmac=PPwpqfjXOagQmhd_K7H4NXyA4B6svToDi1IbkDW2Eos" />
</div>,
div
);
editor.selection.setNode(div);
},
},
];
callback(items as any);
},
});
};

useRef undefined error using nextjs and remirror editor component

I'm using NextJS and Remirror Editor plugin. I get an error that setContent is undefined on the index page where the editor is loaded. I want to add an "external" button after the Component is loaded to exit the text. The component is dynamically externally loaded. I'm really unsure how to make the external button/text change to work.
Index.tsx:
import { NextPage, GetStaticProps } from 'next';
import dynamic from 'next/dynamic';
import { WysiwygEditor } from '#remirror/react-editors/wysiwyg';
import React, { useRef, useEffect } from 'react';
const TextEditor = dynamic( () => import('../text-editor'), { ssr: false } );
import natural from "natural";
export interface EditorRef {
setContent: (content: any) => void;
}
type Props = {
aaa:string
bbb:string
}
const Home: NextPage< Props > = (props) => {
//hook call ref to use editor externally
const ref = useRef<EditorRef | null>(null);
const {aaa,bbb} = props;
return (
<>
<ul>
{aaa} </ul>
Next.js Home Page
<TextEditor ref={ref} />
{
useEffect(()=>{
ref.current!.setContent({content: "testing the text has changed"})
},[])}
<button onClick={() => ref.current.setContent({content: "testing the text has changed"})}>Set another text button 2</button>
</>
);
};
var stringWordNet = "";
export const getStaticProps = async ():Promise<GetStaticPropsResult<Props>> => {
var wordnet = new natural.WordNet();
wordnet.lookup('node', function(results) {
stringWordNet = String(results[0].synonyms[1]);
});
return {
props:{
aaa:stringWordNet,
bbb:"bbb"
}
}
};
export default Home;
text-editor.tsx
import 'remirror/styles/all.css';
import { useEffect, useState, forwardRef, Ref, useImperativeHandle, useRef } from 'react';
import { BoldExtension,
ItalicExtension,
selectionPositioner,
UnderlineExtension, MentionAtomExtension } from 'remirror/extensions';
import { cx } from '#remirror/core';
import {
EditorComponent,
FloatingWrapper,
MentionAtomNodeAttributes,
Remirror,
useMentionAtom,
useRemirror, ThemeProvider, useRemirrorContext
} from '#remirror/react';
import { css } from "#emotion/css";
const styles = css`
background-color: white;
color: #101010;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px;
`;
const ALL_USERS = [
{ id: 'wordnetSuggestion1', label: 'NotreSuggestion1' },
{ id: 'wordnetSuggestion2', label: 'NotreSuggestion2' },
{ id: 'wordnetSuggestion3', label: 'NotreSuggestion3' },
{ id: 'wordnetSuggestion4', label: 'NotreSuggestion4' },
{ id: 'wordnetSuggestion5', label: 'NotreSuggestion5' },
];
const MentionSuggestor: React.FC = () => {
return (
<FloatingWrapper positioner={selectionPositioner} placement='bottom-start'>
<div>
{
ALL_USERS.map((option, index) => (
<li> {option.label}</li>
))
}
</div>
</FloatingWrapper>
);
};
const DOC = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'New content',
},
],
},
],
};
//Start Imperative Handle Here
export interface EditorRef {
setContent: (content: any) => void;
}
const ImperativeHandle = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const { setContent } = useRemirrorContext({
autoUpdate: true,
});
// Expose content handling to outside
useImperativeHandle(ref, () => ({ setContent }));
return <></>;
});
//Make content to show.
const TextEditor = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const editorRef = useRef<EditorRef | null>(null);
const { manager, state, getContext } = useRemirror({
extensions: () => [
new MentionAtomExtension()
],
content: '<p>I love Remirror</p>',
selection: 'start',
stringHandler: 'html',
});
return (
<>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => editorRef.current!.setContent(DOC)}
></button>
<div className='remirror-theme'>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
<MentionSuggestor />
<ImperativeHandle ref={editorRef} />
</Remirror>
</div>
</>
);
});
export default TextEditor;

Avoid render the button after onClick

I'm trying to render some tags from a source and then put it into a buttons in random form but the issue comes after click the button my component re-render for no reason.
why can i do?...
const Blog = () => {
const [articles, setArticles] = useState([]);
const [tags, setTags] = useState([]);
// consult to database
const getData = async () => {
try {
// get aticles, tags
setArticles([
{ name: "title1", id_tag: 1 },
{ name: "title2", id_tag: 2 }
]);
setTags([
{ id: 1, name: "bell" },
{ id: 2, name: "fashion" }
]);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getData();
}, []);
const getRandomTag = (i) => {
const newTags = tags;
const tag = newTags[Math.floor(Math.random() * newTags.length)];
return (
<button key={i} onClick={() => filterByTag(tag.id)}>
{tag.name}
</button>
);
};
// filter the article by tag
const filterByTag = (id) => {
setArticles(articles.filter((article) => article.id_tag === id));
};
return (
<>
<ul>
{articles.map((article, i) => (
<li key={i}>{article.name}</li>
))}
</ul>
<h4>TAGS</h4>
<ul>{tags.map((tag, i) => getRandomTag(i))}</ul>
</>
);
export default Blog;
https://codesandbox.io/s/heuristic-solomon-sp640j?from-embed=&file=/src/Blog.js
That's because your component re-renders whenever any state inside it or any props it receives changes either by value or reference. And whenever you map something without caching its value it maps on each rerender. You have tu either split it into other component or use caching.
import { useCallback, useEffect, useMemo, useState } from "react";
const Blog = () => {
const [articles, setArticles] = useState([]);
const [tags, setTags] = useState([]);
// consult to database
const getData = async () => {
try {
// get aticles, tags
setArticles([
{ name: "title1", id_tag: 1 },
{ name: "title2", id_tag: 2 },
{ name: "title3", id_tag: 3 },
{ name: "title4", id_tag: 4 },
{ name: "title5", id_tag: 5 },
{ name: "title6", id_tag: 6 }
]);
setTags([
{ id: 1, name: "bell" },
{ id: 2, name: "fashion" },
{ id: 3, name: "fancy" },
{ id: 4, name: "tag" },
{ id: 6, name: "name" },
]);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getData();
}, []);
const filterByTag = useCallback((id) => {
setArticles(currentArticles => currentArticles.filter((article) => article.id_tag === id));
}, [setArticles]);
const getRandomTag = useCallback(
(i) => {
const newTags = tags;
const tag = newTags[Math.floor(Math.random() * newTags.length)];
return (
<button key={i} onClick={() => filterByTag(tag.id)}>
{tag.name}
</button>
);
},
[tags, filterByTag]
);
// filter the article by tag
const renderTags = useMemo(() => {
return tags.map((tag, i) => getRandomTag(i));
}, [tags, getRandomTag]);
return (
<>
<ul>
{articles.map((article, i) => (
<li key={i}>{article.name}</li>
))}
</ul>
<h4>TAGS</h4>
<ul>{renderTags}</ul>
</>
);
};
export default Blog;
Here you have a working example of caching https://codesandbox.io/s/sad-shadow-8pogoc?file=/src/Blog.js

"Missing config for event" using #xstate/test

I'm testing a state machine using model-based testing using #xstate/test and #testing-library/react.
Basically, I'm testing this machine:
const itemDamagedMachine = createMachine({
initial: 'newModal',
context: {
productScan: '',
binScan: '',
},
states: {
newModal: {
initial: 'scanDamagedItem',
states: {
scanDamagedItem: {},
scanDamagedBin: {},
declareItemDamaged: {},
},
},
closed: {},
},
on: {
UPDATE_PRODUCT_SCAN: {
actions: assign({
productScan: 123456,
}),
},
VALIDATE: {
target: 'newModal.scanDamagedBin',
},
UNREADABLE: {
target: 'newModal.scanDamagedBin',
},
CANCEL: {
target: 'closed',
},
UPDATE_DAMAGED_BIN_SCAN: {
actions: assign({
binScan: 'PB_DAMAGED',
}),
},
},
});
I'm then configuring the model, and testing it using const testPlans = itemDamagedModel.getSimplePathPlans();.
Everything seems to run smoothly with about 200 passing tests, but I'm having a few issues:
For each of my test and each of my event, I'm getting a warning Missing config for event "VALIDATE". I don't understand what it's supposed to mean.
All of my tests are validated even if I make typos on purpose in my model event. Sometimes the number of tests is reduced, but I would have hoped to see a few warnings when the model doesn't find a particular input or button.
The tests are all passing, even if I'm passing an empty div as my xstate/test rendered component.
I do not get the idea, but I have tested a component as follow:
First I have my machine:
import { createMachine, sendParent } from 'xstate';
export const machineDefinition = {
id: 'checkbox',
initial: 'unchecked',
states: {
unchecked: {
on: {
TOGGLE: [
{
actions: [ 'sendParent' ],
target: 'checked',
},
],
},
},
checked: {
on: {
TOGGLE: [
{
actions: [ 'sendParent' ],
target: 'unchecked',
},
],
},
},
},
};
const machineOptions = {
actions: {
sendParent: sendParent((context, event) => event.data),
},
};
export default createMachine(machineDefinition, machineOptions);
Second, I have extended the render method of testing-library
import React from 'react'
import HelmetProvider from 'react-navi-helmet-async'
import SpinnerProvider from '#atoms/GlobalSpinner'
import AlertProvider from '#molecules/GlobalAlert'
import InternationalizationProvider from '#internationalization/InternationalizationProvider'
import { render as originalRender } from '#testing-library/react'
const render = (ui, { locale = 'es', ...renderOptions } = {}) => {
const Wrapper = ({ children }) => {
return (
<InternationalizationProvider>
<AlertProvider>
<SpinnerProvider>
<HelmetProvider>
{children}
</HelmetProvider>
</SpinnerProvider>
</AlertProvider>
</InternationalizationProvider>
)
}
return originalRender(ui, { wrapper: Wrapper, ...renderOptions })
}
export * from '#testing-library/react'
export { render }
Finally, I have created the test
import React from 'react';
import { produce } from 'immer';
import { machineDefinition } from '#stateMachines/atoms/checkbox';
import { createMachine } from 'xstate';
import { createModel } from '#xstate/test';
import { render, cleanup, fireEvent } from '#root/jest.utils';
import Checkbox from '#atoms/Checkbox';
const getMachineDefinitionWithTests = () => produce(machineDefinition, (draft) => {
draft.states.unchecked.meta = {
test: ({ getByTestId }) => {
expect(getByTestId('checkbox-child-3')).toHaveClass('w-8 h-4 rounded-md duration-500 bg-dark-300 dark:bg-accent-100');
},
};
draft.states.checked.meta = {
test: ({ getByTestId }) => {
expect(getByTestId('checkbox-child-3')).toHaveClass('w-8 h-4 rounded-md duration-500 bg-dark-300 dark:bg-accent-100');
expect(getByTestId('checkbox-child-3.1')).toHaveClass('bg-light-100 w-4 h-4 rounded-full duration-500 dark:transform dark:translate-x-full');
},
};
});
const getEvents = () => ({
TOGGLE: {
exec: ({ getByTestId }) => {
fireEvent.click(getByTestId('checkbox-container'));
},
cases: [ {} ],
},
});
describe('checkbox', () => {
const machine = createMachine(getMachineDefinitionWithTests(), {
actions: {
sendParent: () => {},
},
});
const machineModel = createModel(machine)
.withEvents(getEvents());
const testPlans = machineModel.getSimplePathPlans();
testPlans.forEach((plan) => {
describe(plan.description, () => {
afterEach(cleanup);
plan.paths.forEach((path) => {
it(path.description, () => {
const rendered = render(
<Checkbox
test
label='main.txt1'
data={{}}
machine={machine}
/>,
{ locale: 'en' },
);
return path.test(rendered);
});
});
});
});
describe('coverage', () => {
it('should have full coverage', () => {
machineModel.testCoverage();
});
});
});
I have created a react boilerplate which contains XState, there you can find the previous test

Categories

Resources