Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DesignProvider } from "@/app/design/DesignProvider";
import { getUserHandler } from "@/test-lib/handlers/get-user-handler";
import { withAuth } from "@/test-lib/storybook/with-auth";
import { withI18Next } from "@/test-lib/storybook/with-i18next";
import { withReactQuery } from "@/test-lib/storybook/with-react-query";
import { withQueryProvider } from "@/test-lib/storybook/with-query-provider";

export const parameters = {
controls: {
Expand Down Expand Up @@ -40,7 +40,7 @@ export const decorators = [
// eslint-disable-next-line react/no-children-prop, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
(story: any) => createElement(DesignProvider, { children: story() }),
withI18Next,
withReactQuery,
withQueryProvider,
withAuth,
];

Expand Down
10 changes: 7 additions & 3 deletions server/src/db/db.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,10 @@
"id": "4f968992-1aab-49c9-8913-09405915c1c3",
"rating": {
"rate": 2.1,
"count": 430
"count": 431
},
"addedAt": "2025-01-15T10:00:00.000Z",
"updatedAt": null
"updatedAt": "2026-04-21T06:15:55.948Z"
},
{
"id": "4f968992-1aab-49c9-8913-09405915c1c4",
Expand Down Expand Up @@ -518,7 +518,7 @@
{
"id": "00000000-0000-0000-0000-000000000001",
"userId": 1,
"date": "2026-04-14T14:13:41.156Z",
"date": "2026-04-22T18:29:41.007Z",
"products": [
{
"productId": "4f968992-1aab-49c9-8913-09405915c1c8",
Expand All @@ -531,6 +531,10 @@
{
"productId": "4f968992-1aab-49c9-8913-09405915c1c1",
"quantity": 1
},
{
"productId": "4f968992-1aab-49c9-8913-09405915c1c4",
"quantity": 1
}
]
},
Expand Down
7 changes: 7 additions & 0 deletions server/src/modules/carts/carts.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export async function addToCartHandler(
}

const { productId, quantity } = request.body;

const product = db.data.products.find((p) => p.id === productId);
if (!product) {
reply.code(400).send({ message: "Unknown product" });
return;
}

const cart = db.data.carts[index];
const productIndex = cart.products.findIndex(
(p) => p.productId === productId
Expand Down
17 changes: 5 additions & 12 deletions src/features/auth/components/SignInForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, within, screen, expect } from "storybook/test";
import { userEvent, screen, expect } from "storybook/test";

import { postSignInHandler } from "@/test-lib/handlers/post-sign-in-handler";
import { sleep } from "@/test-lib/storybook/sleep";

import { SignInForm } from "./SignInForm";

Expand Down Expand Up @@ -30,23 +29,17 @@ export const WithCredentialsFilledByDefault: Story = {
};

export const SigningIn: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

play: async ({ canvas, step }) => {
await step("Enter credentials", async () => {
await userEvent.type(canvas.getByLabelText(/Username/), "johndoe");
await userEvent.type(canvas.getByLabelText(/Password/), "supersecret");
});

await step("Submit form", async () => {
await sleep(500);

await userEvent.click(canvas.getByRole("button", { name: "Sign in" }));
await sleep(500);
await expect(
await screen.findByText("Successfully signed in!")
).toBeInTheDocument();
});

await expect(
await screen.findByText("Successfully signed in!")
).toBeInTheDocument();
},
};
100 changes: 100 additions & 0 deletions src/features/carts/application/use-add-to-cart.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { act, renderHook } from "@testing-library/react";
import type { PropsWithChildren } from "react";
import { afterEach, beforeEach, expect, it } from "vitest";

import { initializeAuthStore } from "@/features/auth/application/auth-store";
import { USER_CART_ID } from "@/test-lib/fixtures/user-fixture";
import { generateUuid } from "@/test-lib/generate-uuid";
import { errorResponse } from "@/test-lib/handlers/error-responses";
import { putAddToCartHandler } from "@/test-lib/handlers/put-add-to-cart-handler";
import { mswServer } from "@/test-lib/msw-server";
import { TestAuthProvider } from "@/test-lib/TestAuthProvider";
import { TestQueryProvider } from "@/test-lib/TestQueryProvider";
import { spyOnToast } from "@/test-lib/toast-spy";

import { useAddToCart } from "./use-add-to-cart";
import { useProductAddedDialogStore } from "./use-product-added-dialog-store";

beforeEach(() => {
mswServer.use(putAddToCartHandler());
});
afterEach(() => {
useProductAddedDialogStore.setState({ isOpen: false, selectedItem: null });
});

const wrapper = ({ children }: PropsWithChildren) => (
<TestQueryProvider>
<TestAuthProvider>{children}</TestAuthProvider>
</TestQueryProvider>
);

const unauthenticatedWrapper = ({ children }: PropsWithChildren) => {
const store = initializeAuthStore({
isAuthenticated: false,
isError: false,
state: "finished",
});
return (
<TestQueryProvider>
<TestAuthProvider store={store}>{children}</TestAuthProvider>
</TestQueryProvider>
);
};

it("opens the product added dialog and shows a success toast after adding to cart", async () => {
const toastSpy = spyOnToast();
const { result } = renderHook(() => useAddToCart(), { wrapper });

await act(async () => {
await result.current.addToCart(generateUuid());
});

expect(useProductAddedDialogStore.getState().isOpen).toBe(true);
expect(useProductAddedDialogStore.getState().selectedItem).toBe(USER_CART_ID);
expect(toastSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: "success",
description: "A product has been successfully added to your cart.",
})
);
});

it("shows a warning toast and keeps the dialog closed when unauthenticated", async () => {
const toastSpy = spyOnToast();
const { result } = renderHook(() => useAddToCart(), {
wrapper: unauthenticatedWrapper,
});

await act(async () => {
await result.current.addToCart(generateUuid());
});

expect(useProductAddedDialogStore.getState().isOpen).toBe(false);
expect(toastSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: "warning",
description: "Please log in in order to add products.",
})
);
});

it("shows an error toast and keeps the dialog closed when the server returns Unknown product", async () => {
mswServer.use(
putAddToCartHandler(() => errorResponse(400, "Unknown product"))
);
const toastSpy = spyOnToast();
const { result } = renderHook(() => useAddToCart(), { wrapper });

await act(async () => {
await result.current.addToCart(generateUuid());
});

expect(useProductAddedDialogStore.getState().isOpen).toBe(false);
expect(toastSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: "error",
description:
"Product doesn't exist. It may be unavailable or removed from the store.",
})
);
});
1 change: 1 addition & 0 deletions src/features/carts/application/use-add-to-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const useAddToCart = () => {
return;
}

// todo: not implemented yet on the backend
if (e instanceof ProductNotAvailableError) {
notifyProductNotAvailable();
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import {
userEvent,
within,
screen,
waitForElementToBeRemoved,
expect,
Expand All @@ -13,7 +12,6 @@ import {

import { generateUuid } from "@/test-lib/generate-uuid";
import { putAddToCartHandler } from "@/test-lib/handlers/put-add-to-cart-handler";
import { sleep } from "@/test-lib/storybook/sleep";

import { AddToCartButton } from "./AddToCartButton";
import { ProductAddedDialog } from "./ProductAddedDialog";
Expand Down Expand Up @@ -52,9 +50,7 @@ export const Default: Story = {

export const AddingProductToCart: Story = {
...Default,
play: async ({ canvasElement, step }) => {
within(canvasElement);

play: async ({ step }) => {
await step("Add a new product", async () => {
await userEvent.click(
screen.getByRole("button", { name: /Add to cart/ })
Expand All @@ -73,8 +69,6 @@ export const AddingProductToCart: Story = {
});

await step("Acknowledge and continue shopping", async () => {
await sleep(500);

await userEvent.click(
screen.getByRole("button", { name: /Continue shopping/ })
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, within, screen, expect } from "storybook/test";
import { userEvent, screen, expect } from "storybook/test";

import { CheckoutButton } from "@/features/carts/components/CheckoutButton/CheckoutButton";

Expand All @@ -20,9 +20,7 @@ export const Default: Story = {};

export const Purchasing: Story = {
play: async (context) => {
const { canvasElement, step } = context;
within(canvasElement);

const { step } = context;
await step("Clear the cart", async () => {
await userEvent.click(screen.getByRole("button", { name: /Checkout/ }));

Expand Down
30 changes: 11 additions & 19 deletions src/features/carts/components/CheckoutForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, within, screen, expect } from "storybook/test";

import { sleep } from "@/test-lib/storybook/sleep";
import { userEvent, screen, expect } from "storybook/test";

import { CheckoutForm } from "./CheckoutForm";

Expand All @@ -19,35 +17,29 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {};

export const Purchasing: Story = {
play: async ({ canvasElement, step }) => {
within(canvasElement);

play: async ({ step }) => {
await step("Enter credentials", async () => {
await userEvent.type(screen.getByLabelText(/Full Name/i), "John Doe");
await userEvent.type(
screen.getByLabelText(/Address/i),
"NYC Groove Street"
);
await userEvent.click(screen.getByRole("combobox"));
await sleep(100);
await userEvent.click(
screen.getByRole("combobox", { name: /Payment Method/i })
);
await userEvent.click(screen.getByRole("option", { name: "PayPal" }));
await sleep(100);
await expect(screen.getByRole("combobox")).toHaveTextContent("PayPal");
});

await expect(screen.getByRole("combobox")).toHaveTextContent("PayPal");

await step("Submit form", async () => {
await sleep(500);

await userEvent.click(
screen.getByRole("button", { name: "Complete Order" })
);
await expect(
await screen.findByText(
"You have successfully purchased all selected products."
)
).toBeInTheDocument();
});

await expect(
await screen.findByText(
"You have successfully purchased all selected products."
)
).toBeInTheDocument();
},
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { userEvent, within, screen, expect } from "storybook/test";
import { userEvent, screen, expect } from "storybook/test";

import { ClearCartButton } from "@/features/carts/components/ClearCartButton/ClearCartButton";
import { deleteClearCartHandler } from "@/test-lib/handlers/delete-clear-cart-handler";
import { sleep } from "@/test-lib/storybook/sleep";

const meta = {
title: "modules/Carts/ClearCartButton",
Expand All @@ -22,9 +21,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {};

export const ClearingCart: Story = {
play: async ({ canvasElement, step }) => {
within(canvasElement);

play: async ({ step }) => {
await step("Clear the cart", async () => {
await userEvent.click(screen.getByRole("button", { name: /Clear cart/ }));
await expect(
Expand All @@ -33,15 +30,12 @@ export const ClearingCart: Story = {
});

await step("Confirm clearing the cart", async () => {
await sleep(500);

await userEvent.click(
screen.getByRole("button", { name: /Yes, clear cart/ })
);
await expect(
await screen.findByText("Your cart has been successfully cleared.")
).toBeInTheDocument();
});

await expect(
await screen.findByText("Your cart has been successfully cleared.")
).toBeInTheDocument();
},
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { HttpResponse } from "msw";
import { userEvent, screen, expect } from "storybook/test";

import { errorResponse } from "@/test-lib/handlers/error-responses";
import { patchRateProductHandler } from "@/test-lib/handlers/patch-rate-product-handler";
import { withAuth } from "@/test-lib/storybook/with-auth";
import { withoutAuth } from "@/test-lib/storybook/without-auth";
Expand Down Expand Up @@ -56,7 +56,9 @@ export const SubmitRatingFailure: Story = {
parameters: {
msw: {
handlers: [
patchRateProductHandler(() => HttpResponse.json({}, { status: 500 })),
patchRateProductHandler(() =>
errorResponse(500, "Internal Server Error")
),
],
},
},
Expand Down
Loading
Loading