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
Related
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;
I'm having a component that includes 2 switches of Material UI and I want to build a unit test of it.
The component uses data from an API to set the switches off/on default and also has the possibility for the user to click and set it on/off
The data comes and it is set by a custom hook which probably needs to be mocked but I have difficulties doing it having errors
The custom hook is this one
Custom Hook
The goal is to test 4 scenarios
The data comes in that way making the switches off
The data comes in that way making the switches on
The user clicks event to turn on the switches
The user clicks event to turn off the switches
I started by number 1 but don't know how to make it work and this is the unit test I started doing
/**
* #jest-environment jsdom
*/
import { useConfiguration } from '#lib/hooks/useConfiguration';
import { render, screen } from 'test/app-test-utils';
import StudyConfiguration from './StudyConfiguration';
jest.mock('#lib/hooks/useConfiguration');
test.only('renders with SMS messaging and Email replay - all switches off disable', async () => {
jest
.spyOn({ useConfiguration }, 'useConfiguration')
.mockImplementationOnce(() => false)
.mockImplementationOnce(() => {value: 'noreply#test.com'})
.mockReturnValue([
{
scope: 'GLOBAL',
value: 'global#test.com',
},
{ scope: 'DEFAULT', value: 'hello#test.com' },
{ scope: 'STUDY', value: 'noreply#test.com' },
]);
const studyId = { studyId: 'study-1' };
render(<StudyConfiguration studyId={studyId} />);
const replyEmail = screen.getByTestId('email-reply');
const smsMessaging = screen.getByTestId('sms-enable');
// This throws an error if text not matching expect is no need it
screen.getByText(
/allow candidates to reply to emails \(send from global#test\.com instead of noreply#test\.com\)/i,
);
screen.getByText(/sms messaging/i);
expect(replyEmail.querySelector('input[type=checkbox]')).not.toBeChecked();
expect(smsMessaging.querySelector('input[type=checkbox]')).not.toBeChecked();
});
This gave the following errors inside the custom hook and I don't know the right way of mocking it
TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
7 | const intl = useIntl();
8 |
> 9 | const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
| ^
10 | name: 'messaging.email.sender.address',
11 | scope: { studyId },
12 | });
The component
import { useConfiguration } from '#lib/hooks/useConfiguration';
import { FormControlLabel, FormGroup, Switch, Typography } from '#mui/material';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
const StudyConfiguration = ({ studyId }) => {
const intl = useIntl();
const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [smsSettings, setSmsSettings, unsetSmsSettings] = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
const [studyConfOverride, setStudyConfOverride] = useState(
emailSettings?.overrideChain,
);
const [replayEmailAddress, setReplayEmailAddress] = useState(
emailSettings?.value,
);
const [isSmsEnabled, setIsSmsEnabled] = useState(smsSettings?.value);
useEffect(() => {
if (studyConfOverride.length !== emailSettings?.overrideChain.length) {
setStudyConfOverride(emailSettings?.overrideChain);
}
}, [emailSettings?.overrideChain]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (replayEmailAddress !== emailSettings?.value) {
setReplayEmailAddress(emailSettings?.value);
}
}, [emailSettings?.value]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (isSmsEnabled !== smsSettings?.value) {
setIsSmsEnabled(smsSettings?.value);
}
}, [smsSettings?.value]); // eslint-disable-line react-hooks/exhaustive-deps
// Building the default reply email based on 'SCOPE'
// !TODO: study overrides sort in study service (TBD)
let defaultEmail;
if (studyConfOverride?.find(o => o.scope === 'GLOBAL')) {
const { value } = studyConfOverride.find(o => o.scope === 'GLOBAL');
defaultEmail = value;
} else if (studyConfOverride?.find(o => o.scope === 'DEFAULT')) {
const { value } = studyConfOverride.find(o => o.scope === 'DEFAULT');
defaultEmail = value;
}
// Extracting the email domain from default email and used to make a 'noreply#domain.xxx'
const emailDomain = defaultEmail?.substring(defaultEmail.indexOf('#'));
const noReplyEmail = `noreply${emailDomain}`;
const handleReplyEmailChange = async event => {
setReplayEmailAddress(event.target.checked ? defaultEmail : noReplyEmail);
event.target.checked
? await unsetSenderEmail()
: await setSenderEmail(noReplyEmail);
};
const handleSmsConf = async event => {
setIsSmsEnabled(event.target.checked);
event.target.checked
? await unsetSmsSettings()
: await setSmsSettings('false');
};
const isEmailEnabled = replayEmailAddress === defaultEmail;
return (
<FormGroup>
<FormControlLabel
control={
<Switch
data-testid="email-reply"
checked={isEmailEnabled}
onChange={handleReplyEmailChange}
/>
}
label={
<Typography color="textPrimary">
{intl.formatMessage(
{
defaultMessage:
'Allow candidates to reply to emails (send from {replyEmailTxt} instead of {noReplyTxt})',
},
{ replyEmailTxt: defaultEmail, noReplyTxt: noReplyEmail },
)}
</Typography>
}
/>
<FormControlLabel
control={
<Switch
data-testid="sms-enable"
checked={isSmsEnabled}
onChange={handleSmsConf}
/>
}
label={
<Typography color="textPrimary">
{intl.formatMessage({
defaultMessage: `SMS messaging`,
})}
</Typography>
}
/>
</FormGroup>
);
};
export default StudyConfiguration;
From the following section, we are calling the custom hook
const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [smsSettings, setSmsSettings, unsetSmsSettings] = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
I don't know how to mock that and if we console log the first values this is what we have
Email settings:: {
"value": "noreply#test.com",
"overrideChain": [
{
"__typename": "ConfigurationOverrideSlab",
"value": "noreply#test.com",
"scope": "STUDY"
},
{
"__typename": "ConfigurationOverrideSlab",
"value": "global#test.com",
"scope": "GLOBAL"
},
{
"__typename": "ConfigurationOverrideSlab",
"value": "hello#app.trialbee.com",
"scope": "DEFAULT"
}
]
}
I should be able to mock the email settings to show the resulting OBJ and check for that the switches are off.
The other 2 values are functions
setSenderEmail:: ƒ (value) {
return setStudyConfiguration({
variables: {
input: {
name: name,
scope: scope,
value: value
}
}
});
}
unSetSenderEmail:: ƒ () {
return unsetStudyConfiguration({
variables: {
input: _objectSpread({}, input)
}
});
}
I am using Grid.js to render a table in react. I need to extract the data in one of the cells. When I map the args, I get two results back....a MouseEvent and 'n' which contains the data that I need. How do I extract the data out of the 'n' result? Below is an image of what I receive from my current code which is below the picture.
import React, { useState, useEffect, useRef, Fragment } from 'react';
import axios from 'axios';
import { API } from '../../config';
import Layout from '../../components/Layout';
import { Grid, html, h } from 'gridjs';
import 'gridjs/dist/theme/mermaid.css';
const PendingUser = () => {
const [pendingUser, setPendingUser] = useState({});
const wrappedRef = useRef(null);
useEffect(() => {
getPendingUsers();
setPendingUser(pendingUser);
grid.render(wrappedRef.current);
}, []);
const getPendingUsers = async () => {
const { data } = await axios.get(`${API}/admin/pendinguser`);
await data.filter(user => {
user.accountApproved ? setPendingUser(user) : setPendingUser();
});
};
const handleClick = e => {
e.preventDefault();
const buttonValue = e.target.value;
console.log(buttonValue);
grid.on('rowClick', (...args) =>
args.map(data => {
console.log(data);
})
);
};
const grid = new Grid({
search: true,
columns: [
{
name: 'ID',
hidden: true
},
{
name: 'First Name'
},
{
name: 'Last Name'
},
{
name: 'Email'
},
{
name: 'Agency'
},
{
name: 'Approve',
formatter: (cell, row) => {
return h(
'button',
{
style: 'cursor: pointer',
className: 'py-2 mb-2 px-2 border rounded text-white bg-success',
value: 'approve',
onClick: e => handleClick(e, 'value')
},
'Approve'
);
}
},
{
name: 'Deny',
formatter: (cell, row) => {
return h(
'button',
{
styel: 'cursor: pointer',
className: 'py-2 mb-2 px-2 border rounded text-white bg-danger',
value: 'deny',
onClick: e => handleClick(e, 'value')
},
'Deny'
);
}
},
{
name: 'Denied Reason',
formatter: (_, row) =>
html(
'<select>' +
'<center><option value="Non Law Enforcement">Non Law Enforcement</option><option value="Non Law Enforcement">Non US Law Enforcement</option></center>' +
'</select>'
)
}
],
server: {
url: `${API}/admin/pendinguser`,
method: 'GET',
then: data =>
data.map(user => [
user._id,
user.firstName,
user.lastName,
user.email,
user.leAgency
])
}
});
return (
<Layout>
<div ref={wrappedRef} />
</Layout>
);
};
export default PendingUser;
here is what the 'n' data looks like and I have circled what I want to extract.
columns: [{ name: 'Name',
attributes: (cell) => {
// add these attributes to the td elements only
if (cell) {
return {
'data-cell-content':cell,
'onclick': () => alert(cell)
};
}
}},
This worked for me bro.
Let's see we have the simple component ToggleButton:
const ButtonComponent = Vue.component('ButtonComponent', {
props: {
value: Boolean
},
methods: {
handleClick() {
this.$emit('toggle');
}
},
template: `
<button
:class="value ? 'on' : 'off'"
#click="handleClick"
>
Toggle
</button>`
});
And the story for that component:
import ToggleButton from './ToggleButton.vue';
export default {
title: 'ToggleButton',
component: ToggleButton,
argTypes: {
onToggle: {
action: 'toggle' // <-- instead of logging "toggle" I'd like to mutate `args.value` here
}
}
};
export const Default = (_args, { argTypes }) => ({
components: { ToggleButton },
props: Object.keys(argTypes),
template: `
<ToggleButton
:value="value"
:toggle="onToggle"
/>
`
});
Default.args = {
value: false
}
What I want to achieve is to handle toggle action inside the story and change value that I've used in Default.args object to change the button style by changing the class name from .off to .on.
I had the same exact issue, and kept looking for days, till I stumbled upon this github post:
https://github.com/storybookjs/storybook/issues/12006
Currently in my React (am sure vue approach will be similar), I do following:
import React from 'react';
import CheckboxGroupElement from '../CheckboxGroup';
import { STORYBOOK_CATEGORIES } from 'elements/storybook.categories';
import { useArgs } from '#storybook/client-api';
export default {
component: CheckboxGroupElement,
title: 'Components/CheckboxGroup',
argTypes: {
onChange: {
control: 'func',
table: {
category: STORYBOOK_CATEGORIES.EVENTS,
},
},
},
parameters: { actions: { argTypesRegex: '^on.*' } },
};
const Template = (args) => {
const [_, updateArgs] = useArgs();
const handle = (e, f) => {
// inside this function I am updating arguments, but you can call it anywhere according to your demand, the key solution here is using `useArgs()`
// As you see I am updating list of options with new state here
console.log(e, f);
updateArgs({ ...args, options: e });
};
return <CheckboxGroupElement {...args} onChange={handle} />;
};
export const CheckboxGroup = Template.bind({});
CheckboxGroup.storyName = 'CheckboxGroup';
CheckboxGroup.args = {
//Here you define default args for your story (initial ones)
controller: { label: 'Group controller' },
options: [
{ label: 'option 1', checked: true },
{ label: 'option 2', checked: false },
{ label: 'option 3', checked: false },
],
mode: 'nested',
};
According to the Jest test logs, I am missing coverage on line 67 which is present on the chunk of code below
export default compose(
connect(store => ({ // LINE 67
accountGuid: store.global.accountGuid,
shipmentsCSV: store.shipments.shipmentsCSV,
})),
translate(),
)(DownloadCSVItem);
And this is the test I have for that component:
import React from 'react';
import { mount } from 'enzyme';
import DownloadCSVItem from '../../DownloadCSVItem';
import createMockStore from '../../../../utils/createMockStore';
describe('DownloadCSVItem component', () => {
let props;
beforeEach(() => {
props = {
t: k => k,
accountGuid: 'abcd-1234',
shipmentsCSV: {
itemsCount: 2,
shipments: [
{
Courier: 'Hand Delivery',
Note: 'Testing the data transfer request system. ',
},
{
Courier: null,
Note: null,
},
],
},
};
});
it('renders DownloadCSVItem and check if class exists', () => {
const wrapper = mount(
<DownloadCSVItem.WrappedComponent {...props}
store={createMockStore({
global: { accountGuid: props.accountGuid },
shipments: {
totalCount: props.shipmentsCSV.itemsCount,
shipments: props.shipmentsCSV.shipments,
},
})}
/>,
);
expect(wrapper.find('button')).toHaveLength(1);
});
});
I wrote that test and ran the command to test it and it still says coverage on line 67 is missing.
What should I do in this case?