From 30bb25c4168270268cdce876d7693d7959524cf6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 27 May 2026 21:10:01 +0500 Subject: [PATCH] feat: migrate CartSnackbar to global notification store --- client/src/app/App.tsx | 2 - .../cart/add-to-cart/ui/AddToCartButton.tsx | 4 +- .../ui/__tests__/AddToCartButton.test.tsx | 8 +- .../toggle-cart-icon/ui/ToggleCartIcon.tsx | 4 +- .../ui/__tests__/ToggleCartIcon.test.tsx | 12 +-- .../__tests__/cart-notifications.test.ts | 23 ----- client/src/shared/model/cart-notifications.ts | 8 -- client/src/shared/ui/CartSnackbar.tsx | 90 ------------------- .../shared/ui/__tests__/CartSnackbar.test.tsx | 69 -------------- 9 files changed, 14 insertions(+), 206 deletions(-) delete mode 100644 client/src/shared/model/__tests__/cart-notifications.test.ts delete mode 100644 client/src/shared/model/cart-notifications.ts delete mode 100644 client/src/shared/ui/CartSnackbar.tsx delete mode 100644 client/src/shared/ui/__tests__/CartSnackbar.test.tsx diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index e54e482..64654c5 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -1,7 +1,6 @@ import { BrowserRouter } from 'react-router-dom' import { AppProviders } from '@/app/providers/AppProviders' import { AppRoutes } from '@/app/routes' -import { CartSnackbar } from '@/shared/ui/CartSnackbar' import { NotificationStack } from '@/shared/ui/NotificationStack' import { ErrorBoundary } from '@/shared/ui/ErrorBoundary' import { NoiseOverlay } from '@/shared/ui/NoiseOverlay' @@ -13,7 +12,6 @@ export function App() { - diff --git a/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx b/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx index a799881..d68cf17 100644 --- a/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx +++ b/client/src/features/cart/add-to-cart/ui/AddToCartButton.tsx @@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { useUnit } from 'effector-react' import { addToCart } from '@/entities/cart/api/cart-api' import { $user } from '@/shared/model/auth' -import { cartAdded } from '@/shared/model/cart-notifications' +import { addNotification } from '@/shared/model/notification' type Props = { productId: string @@ -21,7 +21,7 @@ export function AddToCartButton(props: Props) { mutationFn: () => addToCart({ productId, qty }), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['me', 'cart'] }) - cartAdded() + addNotification({ type: 'info', message: 'Товар добавлен в корзину' }) }, }) diff --git a/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx b/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx index ee2050d..c1f16e2 100644 --- a/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx +++ b/client/src/features/cart/add-to-cart/ui/__tests__/AddToCartButton.test.tsx @@ -1,7 +1,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as notifications from '@/shared/model/cart-notifications' +import * as notifications from '@/shared/model/notification' import { AddToCartButton } from '../AddToCartButton' vi.mock('@/entities/cart/api/cart-api', () => ({ @@ -21,8 +21,8 @@ describe('AddToCartButton', () => { qc.clear() }) - it('calls cartAdded after successful add', async () => { - const spy = vi.spyOn(notifications, 'cartAdded') + it('calls addNotification after successful add', async () => { + const spy = vi.spyOn(notifications, 'addNotification') render( @@ -32,7 +32,7 @@ describe('AddToCartButton', () => { fireEvent.click(screen.getByRole('button', { name: /в корзину/i })) await vi.waitFor(() => { - expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith({ type: 'info', message: 'Товар добавлен в корзину' }) }) }) }) diff --git a/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx b/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx index af6ef01..fe51801 100644 --- a/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx +++ b/client/src/features/cart/toggle-cart-icon/ui/ToggleCartIcon.tsx @@ -6,7 +6,7 @@ import { ShoppingCart } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { addToCart, fetchMyCart, removeCartItem } from '@/entities/cart/api/cart-api' import { $user } from '@/shared/model/auth' -import { cartAdded } from '@/shared/model/cart-notifications' +import { addNotification } from '@/shared/model/notification' export function ToggleCartIcon(props: { productId: string @@ -31,7 +31,7 @@ export function ToggleCartIcon(props: { mutationFn: () => addToCart({ productId, qty: 1 }), onSuccess: () => { void qc.invalidateQueries({ queryKey: ['me', 'cart'] }) - cartAdded() + addNotification({ type: 'info', message: 'Товар добавлен в корзину' }) }, }) diff --git a/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx b/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx index d90671b..225e153 100644 --- a/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx +++ b/client/src/features/cart/toggle-cart-icon/ui/__tests__/ToggleCartIcon.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { describe, it, expect, vi, beforeEach } from 'vitest' import * as api from '@/entities/cart/api/cart-api' -import * as notifications from '@/shared/model/cart-notifications' +import * as notifications from '@/shared/model/notification' import { ToggleCartIcon } from '../ToggleCartIcon' vi.mock('@/entities/cart/api/cart-api', () => ({ @@ -25,8 +25,8 @@ describe('ToggleCartIcon', () => { qc.clear() }) - it('calls cartAdded after successful add', async () => { - const spy = vi.spyOn(notifications, 'cartAdded') + it('calls addNotification after successful add', async () => { + const spy = vi.spyOn(notifications, 'addNotification') render( @@ -38,15 +38,15 @@ describe('ToggleCartIcon', () => { fireEvent.click(screen.getByRole('button', { name: /в корзину/i })) await vi.waitFor(() => { - expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith({ type: 'info', message: 'Товар добавлен в корзину' }) }) }) - it('does not call cartAdded on remove', async () => { + it('does not call addNotification on remove', async () => { vi.mocked(api.fetchMyCart).mockResolvedValueOnce({ items: [{ id: 'cart-1', qty: 1, product: { id: 'test-product' } as never }], }) - const spy = vi.spyOn(notifications, 'cartAdded') + const spy = vi.spyOn(notifications, 'addNotification') render( diff --git a/client/src/shared/model/__tests__/cart-notifications.test.ts b/client/src/shared/model/__tests__/cart-notifications.test.ts deleted file mode 100644 index 1c8f1c9..0000000 --- a/client/src/shared/model/__tests__/cart-notifications.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { allSettled, fork } from 'effector' -import { describe, it, expect } from 'vitest' -import { $cartSnackOpen, cartAdded, cartDismissed } from '../cart-notifications' - -describe('cart-notifications store', () => { - it('opens on cartAdded', async () => { - const scope = fork() - await allSettled(cartAdded, { scope }) - expect(scope.getState($cartSnackOpen)).toBe(true) - }) - - it('closes on cartDismissed', async () => { - const scope = fork() - await allSettled(cartAdded, { scope }) - await allSettled(cartDismissed, { scope }) - expect(scope.getState($cartSnackOpen)).toBe(false) - }) - - it('starts closed by default', () => { - const scope = fork() - expect(scope.getState($cartSnackOpen)).toBe(false) - }) -}) diff --git a/client/src/shared/model/cart-notifications.ts b/client/src/shared/model/cart-notifications.ts deleted file mode 100644 index 64f48f0..0000000 --- a/client/src/shared/model/cart-notifications.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createEvent, createStore } from 'effector' - -export const cartAdded = createEvent() -export const cartDismissed = createEvent() - -export const $cartSnackOpen = createStore(false) - .on(cartAdded, () => true) - .on(cartDismissed, () => false) diff --git a/client/src/shared/ui/CartSnackbar.tsx b/client/src/shared/ui/CartSnackbar.tsx deleted file mode 100644 index 27b673f..0000000 --- a/client/src/shared/ui/CartSnackbar.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import Alert from '@mui/material/Alert' -import Button from '@mui/material/Button' -import Snackbar from '@mui/material/Snackbar' -import { useUnit } from 'effector-react' -import { useNavigate } from 'react-router-dom' -import { $cartSnackOpen, cartDismissed } from '@/shared/model/cart-notifications' - -export function CartSnackbar() { - const open = useUnit($cartSnackOpen) - const navigate = useNavigate() - - const handleClose = (_event: React.SyntheticEvent | Event, reason?: string) => { - if (reason === 'clickaway') return - cartDismissed() - } - - const handleGoToCart = () => { - cartDismissed() - navigate('/cart') - } - - return ( - - - Перейти в корзину - - } - > - Товар добавлен в корзину - - - ) -} diff --git a/client/src/shared/ui/__tests__/CartSnackbar.test.tsx b/client/src/shared/ui/__tests__/CartSnackbar.test.tsx deleted file mode 100644 index b163e27..0000000 --- a/client/src/shared/ui/__tests__/CartSnackbar.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { render, screen, fireEvent, act } from '@testing-library/react' -import { MemoryRouter } from 'react-router-dom' -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { cartAdded, cartDismissed } from '@/shared/model/cart-notifications' -import { CartSnackbar } from '@/shared/ui/CartSnackbar' - -const navigateMock = vi.fn() - -vi.mock('react-router-dom', async (importOriginal) => { - const mod = await importOriginal() - return { ...mod, useNavigate: () => navigateMock } -}) - -beforeEach(() => { - navigateMock.mockClear() -}) - -afterEach(() => { - vi.useRealTimers() -}) - -function renderWithRouter() { - render( - - - , - ) -} - -describe('CartSnackbar', () => { - it('is hidden when store is false', () => { - cartDismissed() - renderWithRouter() - expect(screen.queryByText(/товар добавлен/i)).not.toBeInTheDocument() - }) - - it('shows snackbar when cartAdded is fired', () => { - renderWithRouter() - cartAdded() - expect(screen.getByText(/товар добавлен/i)).toBeInTheDocument() - expect(screen.getByRole('button', { name: /перейти в корзину/i })).toBeInTheDocument() - }) - - it('closes on dismiss button click', () => { - renderWithRouter() - cartAdded() - const closeBtn = screen.getByLabelText(/закрыть/i) - fireEvent.click(closeBtn) - expect(screen.queryByText(/товар добавлен/i)).not.toBeInTheDocument() - }) - - it('auto-closes after 4 seconds', () => { - vi.useFakeTimers() - renderWithRouter() - cartAdded() - act(() => { - vi.advanceTimersByTime(4000) - }) - expect(screen.queryByText(/товар добавлен/i)).not.toBeInTheDocument() - }) - - it('navigates to /cart and closes on "Перейти в корзину" click', () => { - renderWithRouter() - cartAdded() - fireEvent.click(screen.getByRole('button', { name: /перейти в корзину/i })) - expect(navigateMock).toHaveBeenCalledWith('/cart') - expect(screen.queryByText(/товар добавлен/i)).not.toBeInTheDocument() - }) -})