I’m relatively new to the world of writing unit tests and using Vitest so my issue might be quite obvious to someone more experienced but I can’t for the life of me get the third test to actually remove the line item the test is targeting. Despite trying lots of different approaches the result is always the same, that the item shouldn’t still be in the cart but it is.
For some additional context, I’m using:
- Vite + React
- React router
- Radix themes
- Vitest
- React testing library
- Jest DOM
I can verify that the button exists within the test but for some reason it persists. I’m not clear if it’s an issue with mocking the click event on the button or state management.
Here’s the CartDrawer component:
import { forwardRef } from "react";
import { Link } from "react-router-dom";
import {
Badge,
Box,
Button,
Flex,
Heading,
IconButton,
Section,
Table,
Text,
} from "@radix-ui/themes";
import QuantityInput from "../components/QuantityInput";
import Price from "../components/Price";
import TrashCan from "/src/assets/svg/trash-can.svg?react";
import { motion, AnimatePresence } from "framer-motion";
import PropTypes from "prop-types";
const CartDrawer = forwardRef(function CartDrawer(
{ cart, setCart, drawerIsOpen, setDrawerIsOpen },
ref
) {
let lineItemTotal = 0;
const variants = {
initial: {
opacity: 0,
right: "-100%",
x: "100%",
position: "fixed",
zIndex: 100,
},
open: {
opacity: 1,
right: 0,
x: 0,
position: "fixed",
zIndex: 100,
},
closed: {
opacity: 0,
right: "-100%",
x: "100%",
position: "fixed",
zIndex: 100,
},
};
const handleCartCloseBtnClick = () => {
setDrawerIsOpen(!drawerIsOpen);
};
const handleRemoveItem = (e) => {
const lineItemId = e.target.dataset.productId;
const updatedCart = cart.filter(
(cartItem) => parseInt(lineItemId) !== cartItem.id
);
setCart(updatedCart);
};
const lineItems = cart.map((lineItem) => {
lineItemTotal = lineItemTotal + lineItem.price * lineItem.quantity;
return (
<Table.Row key={lineItem.id}>
<Table.RowHeaderCell>
<Flex align='start' direction='column' gap='3'>
<Flex gap='3' justify='start'>
<Link to={`/products/${lineItem.handle}`}>
<img
src={lineItem.image}
alt={lineItem.title}
className='max-[380px]:max-w-16 max-w-[6rem] rounded-md'
/>
</Link>
<Flex direction='column' gap='2'>
<Link to={`/products/${lineItem.handle}`}>
<Text size='3'>{lineItem.title}</Text>
</Link>
<Flex gap='2'>
<Price>
<Text as='p' size={{ initial: "2", sm: "3" }} mr='.5'>
{lineItem.price}
</Text>
</Price>
{lineItem.price < lineItem.compare_at_price && (
<>
<Price>
<Text
as='p'
size={{ initial: "2", sm: "3" }}
className='line-through'
>
{lineItem.compare_at_price}
</Text>
</Price>
<Badge color='crimson'>On Sale!</Badge>
</>
)}
</Flex>
<Flex
gap='3'
direction='row'
wrap='nowrap'
justify='start'
align='center'
>
<QuantityInput
cart={cart}
setCart={setCart}
product={lineItem}
quantity={lineItem.quantity}
updateCart={true}
className='text-sm'
/>
<IconButton
variant='solid'
highContrast
className='remove-btn hover:cursor-pointer'
onClick={handleRemoveItem}
data-testid={`remove-button-${lineItem.id}`}
>
<TrashCan className='p-1' data-product-id={lineItem.id} />
</IconButton>
</Flex>
</Flex>
</Flex>
</Flex>
</Table.RowHeaderCell>
<Table.Cell>
<Flex justify='start'>
<Price>
<Text as='p' size='3' mr='.5'>
{(lineItem.price * lineItem.quantity).toLocaleString()}
</Text>
</Price>
</Flex>
</Table.Cell>
</Table.Row>
);
});
return (
<>
<AnimatePresence mode='wait' key={location.pathname}>
{drawerIsOpen && (
<motion.aside
initial={"initial"}
animate={drawerIsOpen ? "open" : "closed"}
exit={"closed"}
variants={variants}
transition={{ duration: 0.35, ease: "easeInOut" }}
id='cart-drawer'
className='cart-drawer fixed top-0 right-0 h-full max-[420px]:w-full sm:max-w-[400px] flex flex-col bg-white shadow-lg'
ref={ref}
>
{/* Header Section */}
<div className='cart-drawer-header flex-shrink-0 p-2 border-b'>
<Flex justify='between' align='center' direction='row'>
<Heading
as='h1'
size='5'
align='left'
trim='both'
className='uncial-antiqua-regular uppercase'
>
Cart
</Heading>
<Button
highContrast
variant='outline'
className='hover:cursor-pointer'
onClick={handleCartCloseBtnClick}
>
×
</Button>
</Flex>
</div>
{/* Scrollable Line Items Section */}
<div className='cart-drawer-line-items flex-grow overflow-auto '>
{cart.length > 0 ? (
<Box>
<Table.Root variant='ghost'>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Product</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Total</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>{lineItems}</Table.Body>
</Table.Root>
</Box>
) : (
<Section className='flex flex-col items-center justify-center h-full'>
<Text as='p' size='6' className='text-center max-w-[350px]'>
Your cart is empty, Dear Traveler. Please consider looking
over some of our humble offerings.
</Text>
<Button highContrast>
<Link to='/collections/all'>View All Products</Link>
</Button>
</Section>
)}
</div>
{/* Footer Section */}
<div className='cart-drawer-footer flex-shrink-0 px-4 pt-3 pb-5 border-t'>
<Flex justify='end' align='end' direction='column' gap='4'>
<Flex justify='end' gap='2' className='w-full'>
<Text as='p' size='3' mr='.5' align='right'>
Subtotal:
</Text>
<Price className='text-right'>
<Text as='p' size='3' mr='.5' className=''>
{lineItemTotal.toLocaleString()}
</Text>
</Price>
</Flex>
<Link to={"/cart"} className='w-full'>
<Button
className='hover:cursor-pointer mb-2 min-w-[100%!important] py-[1.25rem!important]'
highContrast
>
View Cart
</Button>
</Link>
</Flex>
</div>
</motion.aside>
)}
</AnimatePresence>
</>
);
});
CartDrawer.propTypes = {
cart: PropTypes.array,
setCart: PropTypes.func,
drawerIsOpen: PropTypes.bool,
setDrawerIsOpen: PropTypes.func,
};
export default CartDrawer;
The CartDrawer.test.jsx file:
import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, beforeEach, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import CartDrawer from "../components/CartDrawer";
import { MemoryRouter } from "react-router-dom";
import { createContext, useState } from "react";
// Mock ResizeObserver
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = ResizeObserver;
const MockCartContext = createContext();
const MockCartProvider = ({ children, cart, setCart }) => (
<MockCartContext.Provider value={{ cart, setCart }}>
{children}
</MockCartContext.Provider>
);
describe("CartDrawer Component", () => {
const initialCart = [
{
id: 345098765432,
title: "Potion of Healing",
image: "/images/product-images/potion-of-healing.jpeg",
price: 50,
quantity: 1,
compare_at_price: 50,
tags: ["potions", "healing", "consumables"],
available: 10,
handle: "potion-of-healing",
},
{
id: 234567890123,
title: "Elixir of Strength",
image: "/images/product-images/elixir-of-strength.jpeg",
price: 90,
quantity: 1,
compare_at_price: 100,
tags: ["elixirs", "strength", "consumables", "sale"],
available: 8,
handle: "elixir-of-strength",
},
];
const Wrapper = ({ children, cart, setCart }) => (
<MemoryRouter>
<MockCartProvider cart={cart} setCart={setCart}>
{children}
</MockCartProvider>
</MemoryRouter>
);
let cartState;
let setCartMock;
beforeEach(() => {
cartState = [...initialCart];
setCartMock = vi.fn((newCart) => {
cartState = newCart;
});
render(
<Wrapper cart={cartState} setCart={setCartMock}>
<CartDrawer
cart={cartState}
setCart={setCartMock}
drawerIsOpen={true}
setDrawerIsOpen={vi.fn()}
/>
</Wrapper>
);
});
it("renders cart items correctly", () => {
initialCart.forEach((item) => {
expect(screen.getByText(item.title)).toBeInTheDocument();
});
});
it("calculates total price correctly", () => {
const totalPrice = initialCart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
expect(screen.getByText("Subtotal:")).toBeInTheDocument();
expect(screen.getByText(totalPrice.toLocaleString())).toBeInTheDocument();
});
it("removes item from cart on click", async () => {
const removeButton = screen.getByTestId("remove-button-345098765432");
console.log("Remove Button Found:", removeButton);
await userEvent.click(removeButton);
// Mock the state change by updating the cart
const updatedCart = initialCart.filter((item) => item.id !== 345098765432);
setCartMock(updatedCart);
await waitFor(() => {
console.log("Checking if 'Potion of Healing' is in the document...");
expect(screen.queryByText("Potion of Healing")).not.toBeInTheDocument();
});
expect(setCartMock).toHaveBeenCalledWith(
initialCart.filter((item) => item.id !== 345098765432)
);
});
});
And the error I’m getting:
FAIL src/tests/CartDrawer.test.jsx > CartDrawer Component > removes item from cart on click
Error: expect(element).not.toBeInTheDocument()
expected document not to contain element, found <span
class="rt-Text rt-r-size-3"
>
Potion of Healing
</span> instead