How to write unittest for table row selection using Antd Library in React - antd

I have a react application and I'm using #testing-library/jest-dom for unit testing and antd 3.x library for UI design. In one of my UI screen there is a table and a button where the button only enables when one of the row in table is checked. So I wanted to do a unittest for this. So below is my src code,
import {Modal, Button, Form, Input, DatePicker, Table, Card, Tooltip} from 'antd';
...
...
return (
<>
<Form>
...
<Card>
<Table
data-testid={'lift-hold-grid'}
className="holdListResultsTable"
rowSelection={rowSelection}
columns={columns}
dataSource={dataSourcee}
components={{
body: {
row: showRestorationTooltip
}
}}
/>
</Card>
...
<div style = {...}>
<Button data-testid={'lift-hold-button'} disabled={...} onClick={...}>
Lift Hold
</Button>
</div>
<Form>
</>
)
and below is the unittest
import { render } from "#testing-library/react";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
function renderWithReduxAndThunk(ui, initialState) {
const createdStore = createStore(rootReducer, initialState, applyMiddleware(thunk));
return {
...render(<Provider store={createdStore}>{ui}</Provider>),
createdStore,
}
}
const changedState = {
...
}
it('should enable the lift hold button after checkbox selection and note input', () => {
const { container, getByTestId } = renderWithReduxAndThunk(<LegalHoldLiftScreen />, changedState);
const checkBox = container.querySelector('.ant-checkbox-input');
fireEvent.click(checkBox);
const downloadButton = getByTestId('lift-hold-button');
expect(downloadButton).toBeTruthy();
expect(downloadButton).not.toBeDisabled();
});
but this fails with below message
● should enable the lift hold button after checkbox selection and note input
Unable to fire a "click" event - please provide a DOM element.
what am I missing here??

Related

How do I use slots with a Quasar Dialog Plugin custom component?

I want to make a custom component for the Quasar Dialog. And inside that component I want to use slots, but I'm not sure how to do that.
This is my CustomDialogComponent.vue where I have defined a cancelBtn slot and a confirmBtn slot:
<template>
<!-- notice dialogRef here -->
<q-dialog ref="dialogRef" #hide="onDialogHide">
<q-card class="q-dialog-plugin">
<q-card-section>
<strong>{{ title }}</strong>
</q-card-section>
<q-card-section>
<slot name="cancelBtn" #click="handleCancelClick"></slot>
<slot name="confirmBtn" #click="handleConfirmClick"></slot>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { PropType } from 'vue';
import { useDialogPluginComponent } from 'quasar';
defineProps({
title: {
type: String,
required: false,
default: 'Alert',
},
});
defineEmits([
...useDialogPluginComponent.emits,
]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
const handleConfirmClick = () => {
console.log('Confirm Button Clicked');
onDialogOK();
};
const handleCancelClick = () => {
console.log('Cancel Button Clicked');
onDialogCancel();
};
</script>
And the Quasar docs show that I can invoke it via a $q.dialog({ ... }) Object. With props etc all set inside that object. So that would look something like this:
<template>
<div #click="showDialog">Show The Dialog</div>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar';
import CustomDialogComponent from 'src/components/CustomDialogComponent.vue'
const $q = useQuasar();
const showDialog = () => {
$q.dialog({
component: CustomDialogComponent,
// props forwarded to your custom component
componentProps: {
title: 'Alert title goes here',
},
})
};
</script>
But there are no properties inside the Dialog Object for me to pass in my slots. So where can I pass in the cancelBtn and confirmBtn slots I created in CustomDialogComponent.vue?
I asked directly and apparently there is no way to use slots at this time. They might add this functionality later.

react-hook-form custom resolver only checking after submit

I'm building an abstract form component with react-hook-form and Yup for validation. The form works, and validation works, but only after the submit button is pressed.
It's on codesandbox, but ...
import React, { cloneElement } from "react";
import "./styles.css";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { yupResolver } from "#hookform/resolvers/yup";
import { string as yupString, object as yupObject } from "yup";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
TextField
} from "#mui/material";
let renderCount = 0;
export const FormContent = ({ content }) => {
return content.map((item, i) => {
const name = item.component.props.name;
return (
<Controller
key={name + "_" + i}
name={name}
defaultValue=""
render={({ field, fieldState: { error }, formState: { isDirty } }) => {
return cloneElement(item.component, {
...field,
error: isDirty && !!error,
helperText: isDirty && error?.message,
FormHelperTextProps: { error: true }
});
}}
/>
);
});
};
export default function App() {
renderCount++;
const usernameInput = {
validation: yupString().required("Username is required"),
component: (
<TextField required label="Username" name="username" type="text" />
)
};
const passwordInput = {
validation: yupString().required("Password is required"),
component: <TextField required label="Password" name="password" />
};
const content = [usernameInput, passwordInput];
let validationSchema = yupObject().shape({});
// construct schema
content.forEach((item) => {
validationSchema = validationSchema.concat(
yupObject().shape({
[item.component.props.name]: item.validation
})
);
});
const methods = useForm({
resolver: yupResolver(validationSchema)
});
const onFormSubmit = (data) => {
console.log(data);
};
return (
<Dialog open>
<Box>Render Count: {renderCount}</Box>
<FormProvider {...methods}>
<Box component="form" onSubmit={methods.handleSubmit(onFormSubmit)}>
<DialogContent>
<FormContent content={content} />
</DialogContent>
<DialogActions>
<Button
type="submit"
fullWidth
name="login"
variant="contained"
color="primary"
size="large"
>
Login
</Button>
</DialogActions>
</Box>
</FormProvider>
</Dialog>
);
}
If you type some data in the fields, and then erase the data without pressing the button, nothing happens. If you leave the fields empty and press the button, it gives the native component error message for required (i.e., it doesn't do the Yup resolving). But, if you enter some data, press the button, and then erase the data, then the Yup validation kicks in. How do I make it work before the button is pressed?
You need to remove required prop from input components because otherwise native html validation will kick in.
And if you want start validation before pressing submit button you need to use some other mode for form, for example:
const methods = useForm({
resolver: yupResolver(validationSchema),
mode: 'onChange' // or 'onBlur' for example
});
Codesandbox
More info in the docs

How to keep popup of Quasar Select component open?

I'm working to create a geocoding component that allows a user to search for their address, using Quasar's <q-select /> component. I'm running in to one issue with the popup however.
After a user enter's the search query, I fetch the results from an API and the results are set to a reactive local state (which populates the select's options). Instead of the popup displaying though, it closes, and I have to click on the chevron icon twice for the popup to display the results.
This first image is what it looks like when I first click in to the input.
The second image shows what happens after entering a query. The data is fetched, options are set, and the popup closes.
The third image shows the select after clicking on the chevron icon twice.
How do I programmatically show the popup, so that once the results are fetched, the popup is displayed correctly?
Edit: Created a working repro here.
<template>
<q-select
ref="geolocateRef"
v-model="state.location"
:options="state.locations"
:loading="state.loadingResults"
clear-icon="clear"
dropdown-icon="expand_more"
clearable
outlined
:use-input="!state.location"
dense
label="Location (optional)"
#clear="state.locations = undefined"
#input-value="fetchOptions">
<template #prepend>
<q-icon name="place " />
</template>
<template #no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
</template>
<script lang='ts' setup>
import { reactive } from 'vue';
import { debounce, QSelect } from 'quasar';
import { fetchGeocodeResults } from '#/services';
const state = reactive({
location: undefined as string | undefined,
locations: undefined,
loadingResults: false,
geolocateRef: null as QSelect | null,
});
const fetchOptions = debounce(async (value: string) => {
if (value) {
state.loadingResults = true;
const results = await fetchGeocodeResults(value);
state.locations = results.items.map(item => ({
label: item.title,
value: JSON.stringify(item.position),
}));
state.loadingResults = false;
state.geolocateRef?.showPopup(); // doesn't work?
}
}, 500);
</script>
I'd also posted this question over in the Quasar Github discussions, and someone posted a brilliant solution.
<template>
<q-select
v-model="state.location"
:use-input="!state.location"
input-debounce="500"
label="Location (optional)"
:options="options"
dense
clear-icon="bi-x"
dropdown-icon="bi-chevron-down"
clearable
outlined
#filter="fetchOptions">
<template #prepend>
<q-icon name="bi-geo-alt" />
</template>
<template #no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
</template>
<script lang='ts' setup>
import { reactive, ref } from 'vue';
import { QSelect } from 'quasar';
import { fetchGeocodeResults } from '#/services';
interface Result {
position: {
lat: number;
lng: number;
}
title: string;
}
const state = reactive({
...other unrelated state,
location: undefined as string | undefined,
});
const options = ref([]);
const fetchOptions = async (val: string, update) => {
if (val === '') {
update();
return;
}
const needle = val.toLowerCase();
const results = await fetchGeocodeResults(needle);
options.value = results.items.map((item: Result) => ({
label: item.title,
value: JSON.stringify(item.position),
}));
update();
};
</script>

How to hook data-grid events in parent lit-element component?

I want to react on events started by elements placed in the data-grid rows.
Vaading data-grid prevents events from bubbling up to the parent component containing the grid. Having buttons placed in the grid column rendered for each row I cannot catch the click or any other event from the component that hosts the grid.
The examples from https://vaadin.com/components/vaadin-grid/html-examples are relying on js hooks being attached in the html file. I am working with Lit-element and trying to do the same at firstUpdated() callback. The problem is that apparently at this point the table is not available.
<vaadin-grid id="test" .items=${this.data} theme="compact">
<vaadin-grid-column width="40px" flex-grow="0">
<template class="header">#</template>
<template>
<vaadin-button style="font-size:10px;" theme="icon" focus-target #click="${(e) => { console.log(e) }}">
<iron-icon icon="icons:create"></iron-icon>
</vaadin-button>
</template>
</vaadin-grid-column>
</vaadin-grid>
I expected to have the log and nothing happens as the grid component prevents event from bubbling up to my component.
The code that tries to implement renderer property for vaadin-grid-column:
import { LitElement, html, css } from 'lit-element'
import {render} from 'lit-html';
import '#vaadin/vaadin-grid/vaadin-grid.js'
import '#vaadin/vaadin-grid/vaadin-grid-filter-column.js'
import '#vaadin/vaadin-grid/vaadin-grid-sort-column.js';
import '#vaadin/vaadin-grid/vaadin-grid-filter.js';
import '#vaadin/vaadin-grid/vaadin-grid-sorter.js'
export default class MyClass extends LitElement {
static get properties () {
return {
data: {
type: Array,
hasChanged: () => true
},
}
}
get grid() {
return this.shadowRoot.querySelector('vaadin-grid');
}
constructor () {
super()
this.data = []//is being assigned from super as a property to a custom element
}
render () {
return html`
<vaadin-grid id="test" .items=${this.data}>
<vaadin-grid-column .renderer=${this.columnRenderer} header="some header text"></vaadin-grid-column>
</vaadin-grid>
`
}
columnRenderer(root, column, rowData) {
render(html`test string`, root);
}
}
window.customElements.define('my-elem', MyClass)
When using vaadin-grid inside LitElement-based components you should use renderers
Here's how your code would look using renderers
import {LitElement, html} from 'lit-element';
// you need lit-html's render function
import {render} from 'lit-html';
class MyElement extends LitElement {
// we'll just assume the data array is defined to keep the sample short
render() {
return html`
<vaadin-grid id="test" .items=${this.data} theme="compact">
<vaadin-grid-column width="40px" flex-grow="0" .renderer=${this.columnRenderer} .headerRenderer=${this.columnHeaderRenderer}></vaadin-grid-column>
<vaadin-grid>
`;
}
columnHeaderRenderer(root) {
render(html`#`, root);
// you could also just do this
// root.textContent = '#'
// or actually just use the column's header property would be easier tbh
}
columnRenderer(root, column, rowData) {
render(
html`
<vaadin-button style="font-size: 10px;" theme="icon" focus-target #click="${(e) => { console.log(e) }}">
<iron-icon icon="icons:create"></iron-icon>
</vaadin-button>
`, root);
}
}
You can see this and more of vaadin-grid's features in action in LitElement in this Glitch created by one of the vaadin team members

React Bootstrap OverlayTrigger with trigger="focus" bug work around

In iOS safari, OverlayTrigger with trigger="focus" isn't able to dismiss when tapping outside. Here is my code:
<OverlayTrigger
trigger="focus"
placement="right"
overlay={ <Popover id="popoverID" title="Popover Title">
What a popover...
</Popover> } >
<a bsStyle="default" className="btn btn-default btn-circle" role="Button" tabIndex={18}>
<div className="btn-circle-text">?</div>
</a>
</OverlayTrigger>
I know that this is a known bug for Bootstrap cuz this doesn't even work on their own website in iOS, but does anyone know any method to go around it? It would be the best if it is something that doesn't require jQuery, but jQuery solution is welcome. Thanks.
OK, since no one else gives me a work around, I worked on this problem with my co-worker together for 3 days, and we came up with this heavy solution:
THE PROBLEM:
With trigger="focus", Bootstrap Popover/Tooltip can be dismissed when CLICKING outside the Popover/Tooltip, but not TOUCHING. Android browsers apparently changes touches to clicks automatically, so things are fine on Android. But iOS safari and browsers that is based on iOS safari (iOS chrome, iOS firefox, etc...) don't do that.
THE FIX:
We found out that in React Bootstrap, the Overlay component actually lets you customize when to show the Popover/Tooltip, so we built this component InfoOverlay based on Overlay. And to handle clicking outside the component, we need to add event listeners for both the Popover/Tooltip and window to handle both 'mousedown' and 'touchstart'. Also, this method would make the Popover have its smallest width all the time because of the padding-right of the component is initially 0px, and we make based on the width of some parent component so that it is responsive based on the parent component. And the code looks like this:
import React, { Component, PropTypes as PT } from 'react';
import {Popover, Overlay} from 'react-bootstrap';
export default class InfoOverlay extends Component {
static propTypes = {
PopoverId: PT.string,
PopoverTitle: PT.string,
PopoverContent: PT.node,
// You need to add this prop and pass it some numbers
// if you need to customize the arrowOffsetTop, it's sketchy...
arrowOffsetTop: PT.number,
// This is to be able to select the parent component
componentId: PT.string
}
constructor(props) {
super(props);
this.state = {
showPopover: false,
popoverClicked: false
};
}
componentDidMount() {
// Here are the event listeners and an algorithm
// so that clicking popover would not dismiss itself
const popover = document.getElementById('popoverTrigger');
if (popover) {
popover.addEventListener('mousedown', () => {
this.setState({
popoverClicked: true
});
});
popover.addEventListener('touchstart', () => {
this.setState({
popoverClicked: true
});
});
}
window.addEventListener('mousedown', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
window.addEventListener('touchstart', () => {
if (!this.state.popoverClicked) {
this.setState({
showPopover: false
});
} else {
this.setState({
popoverClicked: false
});
}
});
// this is to resize padding-right when window resizes
window.onresize = ()=>{
this.setState({});
};
}
// This function sets the style and more importantly, padding-right
getStyle() {
if (document.getElementById(this.props.componentId) && document.getElementById('popoverTrigger')) {
const offsetRight = document.getElementById(this.props.componentId).offsetWidth - document.getElementById('popoverTrigger').offsetLeft - 15;
return (
{display: 'inline-block', position: 'absolute', 'paddingRight': offsetRight + 'px'}
);
}
return (
{display: 'inline-block', position: 'absolute'}
);
}
overlayOnClick() {
this.setState({
showPopover: !(this.state.showPopover)
});
}
render() {
const customPopover = (props) => {
return (
{/* The reason why Popover is wrapped by another
invisible Popover is so that we can customize
the arrowOffsetTop, it's sketchy... */}
<div id="customPopover">
<Popover style={{'visibility': 'hidden', 'width': '100%'}}>
<Popover {...props} arrowOffsetTop={props.arrowOffsetTop + 30} id={this.props.PopoverId} title={this.props.PopoverTitle} style={{'marginLeft': '25px', 'marginTop': '-25px', 'visibility': 'visible'}}>
{this.props.PopoverContent}
</Popover>
</Popover>
</div>
);
};
return (
<div id="popoverTrigger" style={this.getStyle()}>
<a bsStyle="default" className="btn btn-default btn-circle" onClick={this.overlayOnClick.bind(this)} role="Button" tabIndex={13}>
<div id="info-button" className="btn-circle-text">?</div>
</a>
<Overlay
show={this.state.showPopover}
placement="right"
onHide={()=>{this.setState({showPopover: false});}}
container={this}>
{customPopover(this.props)}
</Overlay>
</div>
);
}
}
In the end, this is a heavy work around because it is a big amount of code for a fix, and you can probably feel your site is slowed down by a tiny bit because of the 4 event listeners. And the best solution is just tell Bootstrap to fix this problem...

Resources