react-tooltip icon indicating copy to clipboard operation
react-tooltip copied to clipboard

Incompatibility with React Testing Library

Open TAGC opened this issue 3 years ago • 2 comments

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>
  );
}

image

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].

TAGC avatar Sep 28 '21 10:09 TAGC

TL;DR but I was able to get consistent snapshots by passing my own unique id for each tooltip as the uuid prop

wmertens avatar Dec 07 '21 17:12 wmertens

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();
});

evsheino avatar Dec 16 '21 11:12 evsheino