react-tooltip
react-tooltip copied to clipboard
Incompatibility with React Testing Library
I've managed to set up some tooltips using this library but I'm having a lot of difficulty updating my tests because it seems like the React Tooltip element does some strange manipulation of the DOM.
I use React Testing Library for my React unit tests. Not only can I not easily test the tooltip itself, but all of the other tests in the same suite are now flaky in the sense that they now pass or fail depending on the order they're run in. This seems to be because the DOM query methods (e.g. getAllByRole
) are including the child elements nested under <ReactTooltip>
on the 2nd+ test that runs in the suite even though this tooltip is supposed to be hidden unless I'm hovering over a specific element.
The React component below renders as a table row, with one of the cells within the row containing a tooltip that displays another table on hovering:
function Order(props: OrderProps) {
const { order } = props;
const completionStatus = getOrderCompletionStatus(order);
return (
<StyledBodyTr>
<StyledTd status={completionStatus}>
<a
onClick={() => trackClick('This period sales table: Order ID')}
href={'/SalesmanOffice2/signatures/printing/Default.aspx?o=' + order.id}
target="_blank"
rel="noreferrer"
>
{order.id}
</a>
</StyledTd>
<StyledTd status={completionStatus}>{order.customerName}</StyledTd>
<StyledTd status={completionStatus}>{order.vehicle}</StyledTd>
<StyledTd status={completionStatus}>{getFormattedDate(order.deliveryDate)}</StyledTd>
<StyledTd status={completionStatus}>{order.status}</StyledTd>
<StyledTd status={completionStatus}>{order.financeDeal ? 'Yes' : 'No'}</StyledTd>
<StyledTd status={completionStatus}>{isAwaitingPayout(order)}</StyledTd>
<StyledTd status={completionStatus} data-tip data-for={`order-commission-${order.id}`}>
{order.commission.total}
<DetailedTooltip
id={`order-commission-${order.id}`}
aria-label={`Order ${order.id} Commission Breakdown`}
place="right"
effect="solid"
type="light"
border={true}
>
<OrderBreakdownTable order={order} />
</DetailedTooltip>
</StyledTd>
<StyledTd status={completionStatus}>{renderEmailStatusBadge(order)}</StyledTd>
</StyledBodyTr>
);
}
DetailedTooltip is defined as:
import React from 'react';
import ReactTooltip, { TooltipProps } from 'react-tooltip';
import styled from 'styled-components';
import './detailedTooltip.css';
type DetailedTooltipProps = {
children: React.ReactNode;
} & Omit<TooltipProps, 'delayShow'>;
const ContentContainer = styled.div`
position: relative;
`;
export default function DetailedTooltip(props: DetailedTooltipProps) {
return (
<ReactTooltip {...props} delayShow={250} role="tooltip">
<div className="__react_component_tooltip_mask" />
<ContentContainer>{props.children}</ContentContainer>
</ReactTooltip>
);
}
Consider these two tests for the code above:
test.only('displays each current period order in a separate row', async () => {
renderCurrentPeriodOrders();
const rows = getTableRows('current-period-orders-table');
const currentPeriodOrders = exampleStatement.orders.filter((x) => x.period === 210);
expect(rows.length).toBe(currentPeriodOrders.length);
});
test.only('displays order details within row', () => {
renderCurrentPeriodOrders();
const rows = getTableRows('current-period-orders-table');
const row = rows.filter((x) => x.textContent?.match(/8677428/))[0];
const cells = getAllByRole(row, 'cell');
screen.debug(row);
expect(cells).toHaveLength(9);
expect(cells[0]).toHaveTextContent('8677428');
expect(cells[1]).toHaveTextContent('Mr GBGVOUCHER GBGVOUCHER');
expect(cells[2]).toHaveTextContent('New AUDI A1 DIESEL HATCHBACK (AB21DEF)');
expect(cells[3]).toHaveTextContent('02/06/2021 08:31');
expect(cells[4]).toHaveTextContent('Delivered');
expect(cells[5]).toHaveTextContent('Yes');
expect(cells[6]).toHaveTextContent('No');
expect(cells[7]).toHaveTextContent('45');
expect(cells[8]).toHaveTextContent('Valid');
});
If I only run the second of these tests in isolation, it passes. If I run both the first and second test, the first one passes and the second one fails because the cell
elements within the React Tooltip component incorrectly get included by the getAllByRow('cell')
query even though the tooltip is supposedly hidden:
● current period orders table › displays order details within row
expect(received).toHaveLength(expected)
Expected length: 9
Received length: 13
Received array: [<td class="sc-lbVvki isgmkS"><a href="/SalesmanOffice2/signatures/printing/Default.aspx?o=8677428" rel="noreferrer" target="_blank">8677428</a></td>, <td class="sc-lbVvki isgmkS">Mr GBGVOUCHER GBGVOUCHER</td>, <td class="sc-lbVvki isgmkS">New AUDI A1 DIESEL HATCHBACK (AB21DEF)</td>, <td class="sc-lbVvki isgmkS">02/06/2021 08:31</td>, <td class="sc-lbVvki isgmkS">Delivered</td>, <td class="sc-lbVvki isgmkS">Yes</td>, <td class="sc-lbVvki isgmkS">No</td>, <td class="sc-lbVvki isgmkS" currentitem="false" data-for="order-commission-8677428" data-tip="true">45<div aria-label="Order 8677428 Commission Breakdown" class="__react_component_tooltip t7739814c-5463-4f8a-8634-4d7fa59d697d place-right type-dark" data-id="tooltip" id="order-commission-8677428" role="tooltip"><style aria-hidden="true">
.t7739814c-5463-4f8a-8634-4d7fa59d697d {
color: #fff;
background: #222;
border: 1px solid transparent;
}·
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-top {
margin-top: -10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-top::before {
border-top: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-top::after {
border-left: 8px solid transparent;
border-right: 8px solid transparent;
bottom: -6px;
left: 50%;
margin-left: -8px;
border-top-color: #222;
border-top-style: solid;
border-top-width: 6px;
}·
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-bottom {
margin-top: 10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-bottom::before {
border-bottom: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-bottom::after {
border-left: 8px solid transparent;
border-right: 8px solid transparent;
top: -6px;
left: 50%;
margin-left: -8px;
border-bottom-color: #222;
border-bottom-style: solid;
border-bottom-width: 6px;
}·
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-left {
margin-left: -10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-left::before {
border-left: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-left::after {
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
right: -6px;
top: 50%;
margin-top: -4px;
border-left-color: #222;
border-left-style: solid;
border-left-width: 6px;
}·
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-right {
margin-left: 10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-right::before {
border-right: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-right::after {
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
left: -6px;
top: 50%;
margin-top: -4px;
border-right-color: #222;
border-right-style: solid;
border-right-width: 6px;
}
</style><div class="__react_component_tooltip_mask" /><div class="sc-ezzafa kaWxuX"><table aria-label="Order 8677428 Commission Breakdown" class="sc-hTRkXV eTGpcK"><thead><tr class="sc-jgPyTC idJOhO"><th class="sc-laZMeE bOBsCF" width="50%">Commission Item</th><th class="sc-laZMeE sc-jffHpj bOBsCF hOLQIo">Commission Paid</th></tr></thead><tbody><tr class="sc-jgPyTC idJOhO"><td class="sc-gSYDnn gsMfut" id="unit-commission">Unit</td><td aria-labelledby="unit-commission" class="sc-gSYDnn sc-oeezt gsMfut dAjQOp">£40</td></tr><tr class="sc-jgPyTC idJOhO"><td class="sc-gSYDnn gsMfut" id="mats">Mats</td><td aria-labelledby="mats" class="sc-gSYDnn sc-oeezt gsMfut dAjQOp">£5</td></tr><tr class="sc-jgPyTC idJOhO"><th class="sc-laZMeE sc-iNiQyp bOBsCF hULHjt">Total</th><th class="sc-laZMeE sc-eJocfa bOBsCF lfTzTR">£45</th></tr></tbody></table></div></div></td>, <td class="sc-gSYDnn gsMfut" id="unit-commission">Unit</td>, <td aria-labelledby="unit-commission" class="sc-gSYDnn sc-oeezt gsMfut dAjQOp">£40</td>, <td class="sc-gSYDnn gsMfut" id="mats">Mats</td>, <td aria-labelledby="mats" class="sc-gSYDnn sc-oeezt gsMfut dAjQOp">£5</td>, <td class="sc-lbVvki isgmkS"><span class="sc-FRrlG iixlwa">Valid</span></td>]
116 |
117 | screen.debug(row);
> 118 | expect(cells).toHaveLength(9);
| ^
119 | expect(cells[0]).toHaveTextContent('8677428');
120 | expect(cells[1]).toHaveTextContent('Mr GBGVOUCHER GBGVOUCHER');
121 | expect(cells[2]).toHaveTextContent('New AUDI A1 DIESEL HATCHBACK (AB21DEF)');
at Object.<anonymous> (src/components/SalesTableTabs/SalesTableTabs.test.tsx:118:19)
console.log
<tr
class="sc-eirqVv dgCmlS"
>
<td
class="sc-lbVvki isgmkS"
>
<a
href="/SalesmanOffice2/signatures/printing/Default.aspx?o=8677428"
rel="noreferrer"
target="_blank"
>
8677428
</a>
</td>
<td
class="sc-lbVvki isgmkS"
>
Mr GBGVOUCHER GBGVOUCHER
</td>
<td
class="sc-lbVvki isgmkS"
>
New AUDI A1 DIESEL HATCHBACK (AB21DEF)
</td>
<td
class="sc-lbVvki isgmkS"
>
02/06/2021 08:31
</td>
<td
class="sc-lbVvki isgmkS"
>
Delivered
</td>
<td
class="sc-lbVvki isgmkS"
>
Yes
</td>
<td
class="sc-lbVvki isgmkS"
>
No
</td>
<td
class="sc-lbVvki isgmkS"
currentitem="false"
data-for="order-commission-8677428"
data-tip="true"
>
45
<div
aria-label="Order 8677428 Commission Breakdown"
class="__react_component_tooltip t7739814c-5463-4f8a-8634-4d7fa59d697d place-right type-dark"
data-id="tooltip"
id="order-commission-8677428"
role="tooltip"
>
<style
aria-hidden="true"
>
.t7739814c-5463-4f8a-8634-4d7fa59d697d {
color: #fff;
background: #222;
border: 1px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-top {
margin-top: -10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-top::before {
border-top: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-top::after {
border-left: 8px solid transparent;
border-right: 8px solid transparent;
bottom: -6px;
left: 50%;
margin-left: -8px;
border-top-color: #222;
border-top-style: solid;
border-top-width: 6px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-bottom {
margin-top: 10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-bottom::before {
border-bottom: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-bottom::after {
border-left: 8px solid transparent;
border-right: 8px solid transparent;
top: -6px;
left: 50%;
margin-left: -8px;
border-bottom-color: #222;
border-bottom-style: solid;
border-bottom-width: 6px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-left {
margin-left: -10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-left::before {
border-left: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-left::after {
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
right: -6px;
top: 50%;
margin-top: -4px;
border-left-color: #222;
border-left-style: solid;
border-left-width: 6px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-right {
margin-left: 10px;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-right::before {
border-right: 8px solid transparent;
}
.t7739814c-5463-4f8a-8634-4d7fa59d697d.place-right::after {
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
left: -6px;
top: 50%;
margin-top: -4px;
border-right-color: #222;
border-right-style: solid;
border-right-width: 6px;
}
</style>
<div
class="__react_component_tooltip_mask"
/>
<div
class="sc-ezzafa kaWxuX"
>
<table
aria-label="Order 8677428 Commission Breakdown"
class="sc-hTRkXV eTGpcK"
>
<thead>
<tr
class="sc-jgPyTC idJOhO"
>
<th
class="sc-laZMeE bOBsCF"
width="50%"
>
Commission Item
</th>
<th
class="sc-laZMeE sc-jffHpj bOBsCF hOLQIo"
>
Commission Paid
</th>
</tr>
</thead>
<tbody>
<tr
class="sc-jgPyTC idJOhO"
>
<td
class="sc-gSYDnn gsMfut"
id="unit-commission"
>
Unit
</td>
<td
aria-labelledby="unit-commission"
class="sc-gSYDnn sc-oeezt gsMfut dAjQOp"
>
£
40
</td>
</tr>
<tr
class="sc-jgPyTC idJOhO"
>
<td
class="sc-gSYDnn gsMfut"
id="mats"
>
Mats
</td>
<td
aria-labelledby="mats"
class="sc-gSYDnn sc-oeezt gsMfut dAjQOp"
>
£
5
</td>
</tr>
<tr
class="sc-jgPyTC idJOhO"
>
<th
class="sc-laZMeE sc-iNiQyp bOBsCF hULHjt"
>
Total
</th>
<th
class="sc-laZMeE sc-eJocfa bOBsCF lfTzTR"
>
£
45
</th>
</tr>
</tbody>
</table>
</div>
</div>
</td>
<td
class="sc-lbVvki isgmkS"
>
<span
class="sc-FRrlG iixlwa"
>
Valid
</span>
</td>
</tr>
at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
Even if I unmount the DOM between each test run, this problem still persists:
let unmountDom: () => void | null;
function renderSalesTableTabs() {
const currentPeriodOrders = exampleStatement.orders.filter((x) => x.period === 210);
const previousPeriodOrders = exampleStatement.orders.filter((x) => x.period === 209);
act(() => {
const { unmount } = render(
<Router>
<SalesTableTabs
productConsultantId={21677}
period={210}
discretionaries={exampleStatement.discretionaries}
currentPeriodOrders={currentPeriodOrders}
previousPeriodOrders={previousPeriodOrders}
supplementaries={exampleStatement.supplementaries}
commission={exampleStatement.commission}
/>
</Router>
);
unmountDom = unmount;
});
}
afterEach(() => {
if (unmountDom) {
unmountDom();
}
});
I also find that while I'm successfully able to test the tooltip appearing when I hover over the appropriate element, I'm not able to write a test for when the tooltip disappears when I unhover over the element. This is the test to ensure it appears (which works fine):
test('displays order commission breakdown as tooltip when hovering over commission', () => {
renderCurrentPeriodOrders();
const rows = getTableRows('current-period-orders-table');
for (const row of rows) {
const orderIdCell = getAllByRole(row, 'cell')[0];
const orderId = Number(orderIdCell.textContent);
const orderCommissionCell = getAllByRole(row, 'cell')[7];
userEvent.hover(orderCommissionCell);
const tooltip = getByRole(orderCommissionCell, 'tooltip', { name: `Order ${orderId} Commission Breakdown` });
const orderBreakdownTable = queryByRole(tooltip, 'table', { name: `Order ${orderId} Commission Breakdown` });
expect(orderBreakdownTable).toBeInTheDocument();
}
});
However, a similar test where I follow this with userEvent.unhover(orderCommissionCell)
and then waitFor
the tooltip to disappear fails. The tooltip apparently remains visible (with visibility: visible
style) until waitFor
times out.
I'm not sure if I'm doing something wrong here. Has anyone managed to get this library to play nicely with React Testing Library? I didn't think it would be this much of a struggle to update my tests to work with this tooltip but I'm reluctant to release this feature until I've got proper test coverage.
I'm using [email protected]
.
TL;DR but I was able to get consistent snapshots by passing my own unique id for each tooltip as the uuid
prop
A style element is added to the document here: https://github.com/wwayne/react-tooltip/blob/018c4bc909a781bacd02438be20441afd54c0944/src/index.js#L208-L216
This element is not automatically removed, and so tests are not independent. As a workaround the element can be removed before or after each test to fix the issue:
beforeEach(() => {
const style = document.querySelector("style[data-react-tooltip]");
style?.remove();
});