My app is a Shopify Hydrogen v2 running in top of Remix Run. I want to implement a test for the AddToFavorites component in order to check that the API and rendering are working well using an isolated mocked Action implemented with CreateRemixStub. Here the files
components/AddToFavoritesButton.jsx
import React, {useCallback} from 'react';
import {useFetcher} from '@remix-run/react';
export default function AddToFavoritesButton({
product_id,
customer_id,
isFavoriteProduct,
}) {
const fetcher = useFetcher();
const addToFavorites = useCallback(() => {
fetcher.submit(
{intent: 'add-to-favorites', product_id, customer_id},
{method: 'post'},
);
}, [fetcher, product_id, customer_id]);
const removeFromFavorites = useCallback(() => {
fetcher.submit(
{intent: 'remove-from-favorites', product_id, customer_id},
{method: 'post'},
);
}, [fetcher, product_id, customer_id]);
return (
<>
{isFavoriteProduct ? (
<button onClick={removeFromFavorites}>Remove from favorites</button>
) : (
<button onClick={addToFavorites}>Add to favorites</button>
)}
</>
);
}
tests/favorite-button.test.js
import React from 'react';
import {render, waitFor, screen, fireEvent} from '@testing-library/react';
import {createRemixStub} from '@remix-run/testing';
import AddToFavoritesButton from '../app/components/AddToFavoritesButton';
import {describe, expect, it, beforeEach, afterEach, vi} from 'vitest';
// Mocking useFetcher from @remix-run/react
vi.mock('@remix-run/react', () => {
return {
useFetcher: vi.fn().mockReturnValue({state: 'idle'}),
};
});
// Simulate the FavoriteProduct module's methods
const FavoriteProduct = {
saveFavorite: vi.fn().mockResolvedValue({success: true}),
deleteFavorite: vi.fn().mockResolvedValue({success: true}),
};
const fakeProduct = {
product_id: '123',
customer_id: 'abc',
isFavoriteProduct: false,
};
describe('AddToFavoritesButton', () => {
let RemixStub;
beforeEach(() => {
RemixStub = createRemixStub([
{
path: '/',
element: <FavoriteProductTest />,
loader: () => fakeProduct,
},
{
path: '/favorites',
action: async ({request}) => {
const formData = await request.formData();
const intent = formData.get('intent');
if (intent === 'add-to-favorites') {
fakeProduct.isFavoriteProduct = true;
await FavoriteProduct.saveFavorite(
formData.get('product_id'),
formData.get('customer_id'),
);
} else if (intent === 'remove-from-favorites') {
fakeProduct.isFavoriteProduct = false;
await FavoriteProduct.deleteFavorite(
formData.get('product_id'),
formData.get('customer_id'),
);
}
return null;
},
},
]);
});
function FavoriteProductTest() {
const {product_id, customer_id, isFavoriteProduct} = fakeProduct;
return (
<AddToFavoritesButton
product_id={product_id}
customer_id={customer_id}
isFavoriteProduct={isFavoriteProduct}
/>
);
}
afterEach(() => {
vi.clearAllMocks();
fakeProduct.isFavoriteProduct = false;
});
it('should add to favorites', async () => {
render(<RemixStub initialEntries={['/']} />);
fireEvent.click(screen.getByText('Add to favorites'));
expect(FavoriteProduct.saveFavorite).toHaveBeenCalledWith(
fakeProduct.product_id,
fakeProduct.customer_id,
);
await waitFor(() =>
expect(screen.getByText('Remove from favorites')).toBeInTheDocument(),
);
expect(fakeProduct.isFavoriteProduct).toBe(true);
});
});
vitest.config.js
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './setup-tests.js', // if you have a setup file
},
});
setup-tests.js
import '@testing-library/jest-dom';
package.json
{
"name": "hydrogen-certification",
"private": true,
"sideEffects": false,
"version": "2024.4.4",
"type": "module",
"scripts": {
"build": "shopify hydrogen build --codegen",
"dev": "shopify hydrogen dev --codegen",
"preview": "npm run build && shopify hydrogen preview",
"lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .",
"codegen": "shopify hydrogen codegen"
},
"prettier": "@shopify/prettier-config",
"dependencies": {
"@remix-run/react": "^2.8.0",
"@remix-run/server-runtime": "^2.8.0",
"@remix-run/testing": "^2.9.2",
"@shopify/cli": "3.59.2",
"@shopify/cli-hydrogen": "^8.0.4",
"@shopify/hydrogen": "2024.4.2",
"@shopify/remix-oxygen": "^2.0.4",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@vitejs/plugin-react": "^4.3.1",
"axios": "^1.7.2",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"isbot": "^3.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vitest": "^1.6.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.2",
"@remix-run/dev": "^2.8.0",
"@remix-run/eslint-config": "^2.8.0",
"@shopify/hydrogen-codegen": "^0.3.1",
"@shopify/mini-oxygen": "^3.0.2",
"@shopify/oxygen-workers-types": "^4.0.0",
"@shopify/prettier-config": "^1.1.2",
"@total-typescript/ts-reset": "^0.4.2",
"@types/eslint": "^8.4.10",
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7",
"eslint": "^8.20.0",
"eslint-plugin-hydrogen": "0.12.2",
"jsdom": "^24.1.0",
"prettier": "^2.8.4",
"typescript": "^5.2.2",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.3.1"
},
"engines": {
"node": ">=18.0.0"
}
}