React-testing-library with Ionic v5 (react) and react-hook-form- change events do not fire - ionic-framework

I am trying to test a component rendered with Controller from react-hook-form with react-testing-library
<Controller
render={({ onChange, onBlur, value }) => (
<IonInput
onIonChange={onChange}
onIonBlur={onBlur}
value={value}
type="text"
data-testid="firstname-field"
/>
)}
name="firstName"
control={control}
defaultValue={firstName}
/>
Default values are as expected when I render the component with a some mock data. However, when I go about changing values, it seems the events are not firing. From this blog post it looks like ionic exports a set of test utils to handle ionic's custom events. After setting that up in my setupTests.ts I'm attempting to use both the ionFireEvent and the fireEvent from RTU, neither of which reflect changes in the component when I use debug(). I've set it up so I can use both fireEvent and ionFireEvent to test:
import { render, screen, wait, fireEvent } from "#testing-library/react";
import { ionFireEvent } from "#ionic/react-test-utils";
// using RTL fireEvent - no change
it("fires change event on firstname", () => {
const { baseElement } = renderGolferContext(mockGolfer);
const firstNameField = screen.getByTestId("firstname-field") as HTMLInputElement;
fireEvent.change(firstNameField, { target: { detail: { value: "Jill" } } });
expect(firstNameField.value).toBe("Jill");
});
// using IRTL ionFireEvent/ionChange - no change
it("fires change event on firstname", () => {
const { baseElement } = renderGolferContext(mockGolfer);
const firstNameField = screen.getByTestId("firstname-field") as HTMLInputElement;
ionFireEvent.ionChange(firstNameField, "Jill");
expect(firstNameField.value).toBe("Jill");
});
screen.debug(baseElement);
I've also tried moving the data-testid property to the controller rather than the IonInput suggested here, with the result being the same: no event is fired.
Here are the versions I'm using:
Using Ionic 5.1.1
#ionic/react-test-utils 0.0.3
jest 24.9
#testing-library/react 9.5
#testing-library/dom 6.16
Here is a repo I've created to demonstrate.
Any help would be much appreciated!

this line appears to be incorrect...
expect(firstNameField.value).toBe("Jill");
It should be looking at detail.value since that is what you set
expect((firstNameField as any).detail.value).toBe("Jill");
this is my test,
describe("RTL fireEvent on ion-input", () => {
it("change on firstname", () => {
const { baseElement, getByTestId } = render(<IonicHookForm />);
const firstNameField = screen.getByTestId(
"firstname-field"
) as HTMLInputElement;
fireEvent.change(firstNameField, {
target: { detail: { value: "Princess" } },
});
expect((firstNameField as any).detail.value).toEqual("Princess");
});
});

Related

Bootstrap-vue modal manipulate ok-disabled state in function

I've set the default OK Button in a Bootstrap-Vue Modal to disabled true and want to change it when inputing something in ab-form-input. Calling the function works but disabling ok-disabled not. Can't get access to the property. Seems to be a very basic question but in the component docs in bootstrap-vue there is only the infor that state can be changed (true-false) but not how to manipulate via script.
`
<template>
<b-modal
id="component-modal"
title="Add Component"
#ok="handleOk"
:ok-disabled="true"
>
<div class="container">
<b-row>
<b-col>Component: </b-col>
<b-col>
<b-form-input
v-model="component"
id="new-component"
required
#input="enableOK"
></b-form-input>
</b-col>
</b-row>
</div>
</b-modal>
</template>
<script>
import axios from 'axios';
import store from '../../store';
export default {
data() {
return {
count: 0,
};
},
methods: {
handleOk() {
this.handleSubmit();
},
handleSubmit() {
this.insertComponentClass(this.component, store.state.project);
delete this.component;
},
insertComponentClass(componentClass, pid) {
const path = `${store.state.apiURL}/componentclass/add`;
const payload = {
name: componentClass,
project_id: pid,
};
axios
.put(path, payload)
.then(() => {
this.$parent.getComponents();
})
.catch((error) => {
console.error(error);
});
},
enableOK() {
console.info('enable ok fired');
this.ok-disable = false; // doesnt wor, linter says "Invalid left-hand side in assignment expression"
},
},
};
</script>
`
There's a few things going on here that are incorrect.
You're binding the ok-disabled prop to a hardcoded value of true in your template. If you want that value to change, you'll need to bind it to a variable that you can update in your components <script>
For example, you can update the modal's :ok-disabled prop to:
:ok-disabled="okDisabled"
Then in your <script> data function, add it to the return object (defaulted to true):
data() {
return {
count: 0,
okDisabled: true,
}
}
Now the modal's :ok-disabled property is bound to that variable and we can change the value in the enableOk method like so:
this.okDisabled = false;
Note regarding the lint error, the name of the variable you're trying to assign to this.ok-disable is not a valid variable name. You can't use a dash (-) character for a Javascript variable name. You can rename it to the property we created earlier this.okDisabled

Unable to pass props to makeStyles when using material UI with next.js

I am using Next.js with material UI.
I have created a Icon and when clicked it calls the setOpenFn() and sets the open variable to be true. This variable is then passed as props to the useStyles(). Now I display the search bar if open is true. But I get the below error
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:67 Warning: Prop `className` did not match. Server: "makeStyles-search-3 makeStyles-search-8" Client: "makeStyles-search-3 makeStyles-search-9"
When the search Icon is clicked display: flex property is also not working.
I tried to create .babelrc file and added this
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}
but still nothing works.
const useStyles = makeStyles((theme) => ({
search: {
[theme.breakpoints.down('sm')]: {
display: (props) => (props.open ? 'flex' : 'none'),
width: '70%',
},
}
}))
const Navbar = () => {
const [open, setOpen] = useState(false);
const setOpenFn = () => {
setOpen(true);
};
const classes = useStyles({ open });
return(
<Search
className={classes.searchButton}
onClick={() => {
setOpenFn();
}}
/>
)
}
I am assuming you are using mui version 5+
According to this migration guide! you need to wrap your JSX with the following component.
<StyledEngineProvider injectFirst>
</StyledEngineProvider>

IonToggle firing twice in React-based Ionic project

In the following block of code, the IonToggle is firing twice for some unknown reason. I had it already replaced with a normal button and it works fine. If I keep the IonToggle and remove the line setUpdating(true) it also works fine.
Is it some known bug, or is there something wrong with this code.
import { AppContext } from './../AppContextProvider';
const LightController: React.FC<InterfaceLamp> = ({ id, color, brightness, turnedOn }) => {
const { state, dispatch } = useContext(AppContext);
const [isUpdating, setUpdating] = useState(false);
const isMount = useIsMount();
const handleUpdateToggle = async (isToggled: boolean) => {
lightService.toggleLight(state.api, id, isToggled, state.auth.username,
state.auth.password).then((res) => {
if (!res.error) {
[...]
dispatch({
key: 'devices',
data: devices,
})
}
setUpdating(false);
})
}
const handleToggle = (isToggled: boolean) => {
setUpdating(true);
handleUpdateToggle(isToggled);
}
return (
<div className="c-light">
<Loader isLoading={isUpdating} message={"Updating devices"} onClose={() => { }} />
<div className="c-light__controls">
<div className="c-light__toggle">
<IonItem lines={"none"}>
<button onClick={(e)=>handleToggle(!turnedOn)}>toggle</button>
<IonToggle checked={turnedOn} onIonChange={(e) => handleToggle(e.detail.checked)}/>
</IonItem>
</div>
</div>
</div>
);
};
export default LightController;
value="true"
why are you setting the value here? dont think it is necessary
As of Ionic React v5.5.0, IonToggle and IonCheckbox have still got the same issue.
The simplest workaround is to add an onClick event listener to the IonItem component that usually wraps the IonToggle component. (Alternately, use any other wrapper component with onClick.) This approach makes it possible to keep the native-looking IonToggle.

Testing Material UI components using Hooks API & Enzyme shallow rendering

The latest versions of Material UI now have a Hooks alternative for styling components, instead of the HoC. So instead of
const styles = theme => ({
...
});
export const AppBarHeader = ({ classes, title }) => (
...
);
export default withStyles(styles)(AppBarHeader);
you can choose to do this instead:
const useStyles = makeStyles(theme => ({
xxxx
}));
const AppBarHeader = ({ title }) => {
const classes = useStyles();
return (
....
)
};
export default AppBarHeader;
In some ways this is nicer, but as with all hooks you can no longer inject a 'stub' dependency to the component. Previously, for testing with Enzyme I just tested the non-styled component:
describe("<AppBarHeader />", () => {
it("renders correctly", () => {
const component = shallow(
<AppBarHeader title="Hello" classes="{}" />
);
expect(component).toMatchSnapshot();
});
});
However, if you use hooks, without the 'stub' dependency for classes, and you get:
Warning: Material-UI: the `styles` argument provided is invalid.
You are providing a function without a theme in the context.
One of the parent elements needs to use a ThemeProvider.
because you always need a provider in place. I can go and wrap this up:
describe("<AppBarHeader />", () => {
it("renders correctly", () => {
const component = shallow(
<ThemeProvider theme={theme}>
<AppBarHeader title="Hello" classes="{}" />
</ThemeProvider>
).dive();
expect(component).toMatchSnapshot();
});
});
but that no longer seems to render the children of the component (even with the dive call). How are folks doing this?
EDIT: As per the comments below, this implementation presents some timing issues. Consider testing with mount instead of shallow testing, or use the withStyles HOC and export your component for shallow rendering.
So I have been grappling with this for a day now. Here is what I have come up with.
There are some issues trying to stub makeStyles since it appears MUI has made it readonly. So instead of creating a useStyles hook in each component, I created my own custom useStyles hook that calls makeStyles. In this way I can stub my useStyles hook for testing purposes, with minimal impact to the flow of my code.
// root/utils/useStyles.js
// My custom useStyles hook
import makeStyles from '#material-ui/styles/makeStyles';
export const useStyles = styles => makeStyles(theme => styles(theme))();
Its almost like using the withStyles HOC
// root/components/MyComponent.js
import React from 'react';
import { useStyles } from '../util/useStyles';
// create styles like you would for withStyles
const styles = theme => ({
root: {
padding: 0,
},
});
export const MyComponent = () => {
const classes = useStyles(styles);
return(
</>
);
}
// root/component/MyComponent.spec.js
import { MyComponent } from './MyComponent';
import { shallow } from 'enzyme';
import { stub } from 'sinon';
describe('render', () => {
it('should render', () => {
let useStylesStub;
useStylesStub = stub(hooks, 'useStyles');
useStylesStub.returns({ });
const wrapper = shallow(<MyComponent />);
console.log('profit');
});
});
This is the best I can come up with for now, but always open to suggetions.

Ember could not get select value from template to component

I'm struggling with this. I would like to pass a select value from template to component.
Here is my template
<select name="bank" class="form-control" id="sel1" onchange={{action "updateValue" value="bank"}}>
{{#each banks as |bank|}}
<option value={{bank.id}}>{{bank.name}}</option>
{{/each}}
{{log bank.id}}
</select>
And here is my component
import Ember from 'ember';
export default Ember.Component.extend({
store: Ember.inject.service('store'),
banks: Ember.computed(function() {
return this.get('store').findAll('bank');
}),
didUpdate() {
const banques = this.get('banks');
const hash = [];
banques.forEach(function(banque) {
hash.push(banque.get('name'));
});
Ember.$(".typeahead_2").typeahead({ source: hash });
},
actions: {
expand: function() {
Ember.$('.custom-hide').attr('style', 'display: block');
Ember.$('.custom-display').attr('style', 'display: none');
},
updateValue(selectedValue) {
this.set('bank.id', selectedValue);
},
login() {
console.log(this.get('bank.id'));
}
}
});
And i've got this beautiful error : Property set failed: object in path "bank" could not be found or was destroyed.
Any idea ? Thanks
When you use value attribute then you need to specify correct property name to be retrieved from the first argument(event). in your case you just mentioned bank - which was not found in event object. that's the reason for that error.
onchange={{action "updateValue" value="target.value"}}
inside component
updateValue(selectedValue) {
this.set('bank.id', selectedValue);
},