codex/frontend-infrastructure-tooling #39

Merged
ksv741 merged 13 commits from codex/frontend-infrastructure-tooling into main 2026-06-23 21:31:16 +03:00
31 changed files with 704 additions and 212 deletions
Showing only changes of commit b26021016c - Show all commits

View File

@ -0,0 +1,11 @@
export const mockUser = {
id: 1,
email: 'user@test.com',
name: 'Test User',
role: 'user',
}
export const mockAuthResponse = {
user: mockUser,
accessToken: 'mock-access-token',
}

View File

@ -0,0 +1,40 @@
export const mockBond = {
secid: 'SU26238RMFS5',
isin: 'RU000A101XU7',
name: 'ОФЗ 26238',
shortName: 'ОФЗ 26238',
latName: null,
listLevel: 1,
issueSize: 500000000,
faceValue: 1000,
faceUnit: 'RUB',
matDate: '2027-05-15',
couponValue: 36.9,
couponPercent: 7.5,
couponPeriod: 182,
nextCoupon: '2024-07-15',
accruedInt: 8.45,
bondType: 'ОФЗ',
bondSubType: 'ОФЗ-ПД',
offerDate: null,
buybackDate: null,
marketData: {
price: 98.5,
yieldToMaturity: 8.2,
duration: 3.5,
accruedInt: 8.45,
couponValue: 36.9,
couponPercent: 7.5,
nextCouponDate: '2024-07-15',
open: 98.0,
high: 99.0,
low: 97.5,
volume: 1000000,
updatedAt: '2024-01-15T10:00:00Z',
},
}
export const mockBondHistory = [
{ date: '2024-01-15', closePrice: 98.5, yieldClose: 8.2, duration: 3.5 },
{ date: '2024-01-14', closePrice: 98.2, yieldClose: 8.3, duration: 3.5 },
]

View File

@ -0,0 +1,189 @@
export const mockBrokerAccounts = [
{
id: '2084014113',
type: 'brokerage',
name: 'Т-Инвестиции',
status: 'open',
openedAt: null,
accessLevel: null,
},
]
export const mockBrokerPortfolio = {
account: mockBrokerAccounts[0],
positionCounts: { shares: 3, bonds: 2, etf: 0, other: 0 },
totals: {
shares: { currency: 'RUB', units: '240000', nano: 500000000, value: 240000.5 },
bonds: { currency: 'RUB', units: '45000', nano: 0, value: 45000 },
etf: null,
currencies: null,
futures: null,
options: null,
structuredProducts: null,
dfa: null,
portfolio: { currency: 'RUB', units: '285000', nano: 500000000, value: 285000.5 },
},
yields: {
expectedPercent: null,
daily: { currency: 'RUB', units: '1200', nano: 0, value: 1200 },
dailyPercent: null,
},
cash: [{ currency: 'RUB', units: '15000', nano: 0, value: 15000 }],
blockedCash: [],
asOf: '2024-06-01T10:00:00Z',
}
export const mockBrokerPositions = [
{
figi: null,
instrumentUid: null,
positionUid: null,
ticker: 'SBER',
classCode: null,
instrumentType: 'share',
name: 'Сбер Банк',
quantity: { currency: '', units: '100', nano: 0, value: 100 },
blockedLots: null,
currentPrice: { currency: 'RUB', units: '289', nano: 500000000, value: 289.5 },
currentValue: { currency: 'RUB', units: '28950', nano: 0, value: 28950 },
averagePositionPrice: null,
expectedYieldPercent: null,
dailyYield: null,
},
{
figi: null,
instrumentUid: null,
positionUid: null,
ticker: 'VTBR',
classCode: null,
instrumentType: 'share',
name: 'ВТБ',
quantity: { currency: '', units: '5000', nano: 0, value: 5000 },
blockedLots: null,
currentPrice: { currency: 'RUB', units: '0', nano: 23400000, value: 0.0234 },
currentValue: { currency: 'RUB', units: '117', nano: 0, value: 117 },
averagePositionPrice: null,
expectedYieldPercent: null,
dailyYield: null,
},
{
figi: null,
instrumentUid: null,
positionUid: null,
ticker: 'SU26238RMFS5',
classCode: null,
instrumentType: 'bond',
name: 'ОФЗ 26238',
quantity: { currency: '', units: '10', nano: 0, value: 10 },
blockedLots: null,
currentPrice: { currency: 'RUB', units: '985', nano: 0, value: 985 },
currentValue: { currency: 'RUB', units: '9850', nano: 0, value: 9850 },
averagePositionPrice: null,
expectedYieldPercent: null,
dailyYield: null,
},
]
export const mockBrokerOperations = [
{
cursor: null,
accountId: '2084014113',
id: 'op-1',
parentOperationId: null,
date: '2024-06-01T10:00:00Z',
type: 'buy',
category: 'trade',
description: null,
name: 'Покупка SBER',
state: 'executed',
instrumentUid: null,
figi: null,
ticker: 'SBER',
classCode: null,
instrumentType: 'share',
payment: { currency: 'RUB', units: '-27500', nano: 0, value: -27500 },
price: { currency: 'RUB', units: '275', nano: 0, value: 275 },
commission: { currency: 'RUB', units: '-55', nano: 0, value: -55 },
yield: null,
accruedInt: null,
quantity: { currency: '', units: '100', nano: 0, value: 100 },
quantityDone: { currency: '', units: '100', nano: 0, value: 100 },
},
{
cursor: null,
accountId: '2084014113',
id: 'op-2',
parentOperationId: null,
date: '2024-05-20T14:30:00Z',
type: 'dividend',
category: 'income',
description: null,
name: 'Дивиденды Сбер',
state: 'executed',
instrumentUid: null,
figi: null,
ticker: 'SBER',
classCode: null,
instrumentType: 'share',
payment: { currency: 'RUB', units: '3500', nano: 0, value: 3500 },
price: null,
commission: null,
yield: null,
accruedInt: null,
quantity: null,
quantityDone: null,
},
]
export const mockBrokerEvents = [
{
id: 'ev-1',
type: 'dividend',
source: 'forecast',
category: 'cashflow',
eventDate: '2024-07-10',
paymentDate: null,
ticker: 'SBER',
name: 'Сбер Банк',
instrumentUid: 'uid-sber',
instrumentType: 'share',
quantitySnapshot: 100,
payoutPerUnit: 35,
estimatedAmount: 3500,
actualAmount: null,
currency: 'RUB',
estimateMode: 'current_position',
},
{
id: 'ev-2',
type: 'coupon',
source: 'forecast',
category: 'cashflow',
eventDate: '2024-07-15',
paymentDate: null,
ticker: 'SU26238RMFS5',
name: 'ОФЗ 26238',
instrumentUid: 'uid-bond',
instrumentType: 'bond',
quantitySnapshot: 10,
payoutPerUnit: 36.9,
estimatedAmount: 369,
actualAmount: null,
currency: 'RUB',
estimateMode: 'current_position',
},
]
export const mockEventsSummary = {
eventCount: 2,
nearestEventDate: '2024-07-10',
totalEstimatedCashflow: 3869,
actualCashflow: 0,
forecastEstimatedCashflow: 3869,
dividendsTotal: 3500,
couponsTotal: 369,
principalRepaymentTotal: 0,
actualDividendsTotal: 0,
actualCouponsTotal: 0,
actualPrincipalRepaymentTotal: 0,
}

View File

@ -0,0 +1,22 @@
export const mockCandles = [
{
open: 280,
high: 290,
low: 278,
close: 289.5,
volume: 1000000,
value: 280000000,
begin: '2024-01-15T10:00:00Z',
end: '2024-01-15T18:00:00Z',
},
{
open: 289,
high: 292,
low: 285,
close: 288,
volume: 800000,
value: 231200000,
begin: '2024-01-16T10:00:00Z',
end: '2024-01-16T18:00:00Z',
},
]

View File

@ -0,0 +1,15 @@
export { mockAuthResponse, mockUser } from './auth'
export { mockBond, mockBondHistory } from './bonds'
export {
mockBrokerAccounts,
mockBrokerEvents,
mockBrokerOperations,
mockBrokerPortfolio,
mockBrokerPositions,
mockEventsSummary,
} from './broker'
export { mockCandles } from './candles'
export { mockAnalytics, mockPortfolioDetail, mockPortfolios, mockPositions } from './portfolios'
export { mockScreenerItems } from './screener'
export { mockSearchResults } from './search'
export { mockDividends, mockShare, mockShareHistory } from './shares'

View File

@ -0,0 +1,138 @@
export const mockPortfolios = [
{
id: 1,
name: 'Основной портфель',
currency: 'RUB',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-06-01T00:00:00Z',
totalValue: 285000,
positionCount: 5,
shareCount: 3,
bondCount: 2,
description: null,
},
{
id: 2,
name: 'ИИС',
currency: 'RUB',
createdAt: '2024-03-15T00:00:00Z',
updatedAt: '2024-06-10T00:00:00Z',
totalValue: 150000,
positionCount: 3,
shareCount: 2,
bondCount: 1,
description: 'Индивидуальный инвестиционный счёт',
},
]
export const mockPositions = [
{
id: 1,
secid: 'SBER',
shortName: 'Сбер',
type: 'share',
quantity: 100,
buyPrice: 275,
currentPrice: 289.5,
totalCost: 27500,
currentValue: 28950,
weightPercent: 10.2,
pnl: 1450,
pnlPercent: 5.27,
dividendIncome: 3500,
totalReturn: 4950,
totalReturnPercent: 18,
change: 14.5,
changePercent: 5.27,
notes: null,
tags: ['DIVIDEND', 'GROWTH'],
},
{
id: 2,
secid: 'VTBR',
shortName: 'ВТБ',
type: 'share',
quantity: 5000,
buyPrice: 0.021,
currentPrice: 0.0234,
totalCost: 105,
currentValue: 117,
weightPercent: 0.04,
pnl: 12,
pnlPercent: 11.43,
dividendIncome: 0,
totalReturn: 12,
totalReturnPercent: 11.43,
change: 0.0024,
changePercent: 11.43,
notes: null,
tags: ['SPECULATIVE'],
},
{
id: 3,
secid: 'SU26238RMFS5',
shortName: 'ОФЗ 26238',
type: 'bond',
quantity: 10,
buyPrice: 97,
currentPrice: 98.5,
totalCost: 9700,
currentValue: 9850,
weightPercent: 3.5,
pnl: 150,
pnlPercent: 1.55,
dividendIncome: 0,
totalReturn: 150,
totalReturnPercent: 1.55,
change: 1.5,
changePercent: 1.55,
yieldToMaturity: 8.2,
duration: 3.5,
couponValue: 36.9,
couponPercent: 7.5,
nextCouponDate: '2024-07-15',
matDate: '2027-05-15',
accruedInt: 8.45,
bid: 98.3,
offer: 98.6,
couponPeriod: 182,
bondType: 'ОФЗ',
},
]
export const mockPortfolioDetail = {
id: 1,
name: 'Основной портфель',
currency: 'RUB',
description: null,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-06-01T00:00:00Z',
positions: mockPositions,
totalValue: 285000,
analytics: {
totalInvested: 250000,
totalValue: 285000,
totalPnl: 35000,
totalPnlPercent: 14,
totalDividends: 5000,
totalReturn: 40000,
totalReturnPercent: 16,
positionCount: 5,
weightedYield: null,
},
}
export const mockAnalytics = {
positions: mockPositions,
summary: {
totalInvested: 250000,
totalValue: 285000,
totalPnl: 35000,
totalPnlPercent: 14,
totalDividends: 5000,
totalReturn: 40000,
totalReturnPercent: 16,
positionCount: 5,
weightedYield: 7.5,
},
}

View File

@ -0,0 +1,38 @@
export const mockScreenerItems = [
{
secid: 'SBER',
shortName: 'Сбер',
isin: 'RU0009029540',
type: 'share',
price: 289.5,
change: 2.5,
changePercent: 0.87,
volume: 15000000,
listLevel: 1,
capitalization: 6250000000000,
},
{
secid: 'VTBR',
shortName: 'ВТБ',
isin: 'RU000A0JP5V6',
type: 'share',
price: 0.0234,
change: 0.0002,
changePercent: 0.86,
volume: 50000000,
listLevel: 1,
capitalization: 30000000000,
},
{
secid: 'GAZP',
shortName: 'Газпром',
isin: 'RU0007661625',
type: 'share',
price: 198.5,
change: -1.5,
changePercent: -0.75,
volume: 8000000,
listLevel: 1,
capitalization: 4700000000000,
},
]

View File

@ -0,0 +1,20 @@
export const mockSearchResults = [
{
secid: 'SBER',
isin: 'RU0009029540',
shortName: 'Сбер',
type: 'share',
listLevel: 1,
currency: 'RUB',
price: 289.5,
},
{
secid: 'VTBR',
isin: 'RU000A0JP5V6',
shortName: 'ВТБ',
type: 'share',
listLevel: 1,
currency: 'RUB',
price: 0.0234,
},
]

View File

@ -0,0 +1,50 @@
export const mockShare = {
secid: 'SBER',
isin: 'RU0009029540',
name: 'Сбер Банк',
shortName: 'Сбер',
latName: 'Sberbank',
listLevel: 1,
issueSize: 21586900000,
faceValue: 3,
faceUnit: 'RUB',
type: 'common_share',
marketData: {
price: 289.5,
change: 2.5,
changePercent: 0.87,
open: 287,
high: 291,
low: 286.5,
volume: 15000000,
value: 4350000000,
issueCapitalization: 6250000000000,
updatedAt: '2024-01-15T10:00:00Z',
},
}
export const mockDividends = [
{ registryCloseDate: '2024-07-10', value: 35.0, currency: 'RUB' },
{ registryCloseDate: '2023-10-05', value: 30.0, currency: 'RUB' },
]
export const mockShareHistory = [
{
date: '2024-01-15',
open: 287,
high: 291,
low: 286.5,
close: 289.5,
volume: 15000000,
value: 4350000000,
},
{
date: '2024-01-14',
open: 285,
high: 288,
low: 284,
close: 287,
volume: 12000000,
value: 3440000000,
},
]

View File

@ -0,0 +1,151 @@
import { HttpResponse, http } from 'msw'
import {
mockAnalytics,
mockAuthResponse,
mockBond,
mockBondHistory,
mockBrokerAccounts,
mockBrokerEvents,
mockBrokerOperations,
mockBrokerPortfolio,
mockBrokerPositions,
mockCandles,
mockDividends,
mockEventsSummary,
mockPortfolioDetail,
mockPortfolios,
mockPositions,
mockScreenerItems,
mockSearchResults,
mockShare,
mockShareHistory,
mockUser,
} from './data'
import { envelope } from './utils'
const API = '/api/v1'
export const handlers = [
http.get(`${API}/health`, () =>
HttpResponse.json(
envelope({ status: 'ok', timestamp: new Date().toISOString(), uptime: 12345 }),
),
),
http.get(`${API}/auth/me`, () => HttpResponse.json(envelope(mockUser))),
http.patch(`${API}/auth/me`, () => HttpResponse.json(envelope({ ...mockUser, name: 'Updated' }))),
http.post(`${API}/auth/login`, () => HttpResponse.json(envelope(mockAuthResponse))),
http.post(`${API}/auth/register`, () => HttpResponse.json(envelope(mockAuthResponse))),
http.post(`${API}/auth/refresh`, () => HttpResponse.json(envelope(mockAuthResponse))),
http.post(`${API}/auth/logout`, () => HttpResponse.json(envelope({ message: 'Logged out' }))),
http.get(`${API}/securities/search`, ({ request }) => {
const url = new URL(request.url)
const q = url.searchParams.get('q') || ''
if (q.length < 2) return HttpResponse.json(envelope([]))
const filtered = mockSearchResults.filter(
(r) =>
r.secid.toLowerCase().includes(q.toLowerCase()) ||
r.shortName.toLowerCase().includes(q.toLowerCase()),
)
return HttpResponse.json(envelope(filtered))
}),
http.get(`${API}/securities/shares/:secid`, ({ params }) => {
const { secid } = params
if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 })
return HttpResponse.json(envelope({ ...mockShare, secid }))
}),
http.get(`${API}/securities/shares/:secid/marketdata`, () =>
HttpResponse.json(envelope(mockShare.marketData)),
),
http.get(`${API}/securities/shares/:secid/dividends`, () =>
HttpResponse.json(envelope(mockDividends)),
),
http.get(`${API}/securities/shares/:secid/history`, () =>
HttpResponse.json(envelope(mockShareHistory)),
),
http.get(`${API}/securities/shares/:secid/candles`, () =>
HttpResponse.json(envelope(mockCandles)),
),
http.get(`${API}/securities/bonds/:secid`, ({ params }) => {
const { secid } = params
if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 })
return HttpResponse.json(envelope({ ...mockBond, secid }))
}),
http.get(`${API}/securities/bonds/:secid/marketdata`, () =>
HttpResponse.json(envelope(mockBond.marketData)),
),
http.get(`${API}/securities/bonds/:secid/history`, () =>
HttpResponse.json(envelope(mockBondHistory)),
),
http.get(`${API}/securities/bonds/:secid/candles`, () =>
HttpResponse.json(envelope(mockCandles)),
),
http.get(`${API}/securities/screener`, () =>
HttpResponse.json(
envelope({
items: mockScreenerItems,
total: mockScreenerItems.length,
page: 1,
pageSize: 20,
totalPages: 1,
}),
),
),
http.get(`${API}/portfolios`, () => HttpResponse.json(envelope(mockPortfolios))),
http.post(`${API}/portfolios`, () => HttpResponse.json(envelope(mockPortfolios[0]))),
http.get(`${API}/portfolios/:id`, () => HttpResponse.json(envelope(mockPortfolioDetail))),
http.patch(`${API}/portfolios/:id`, () => HttpResponse.json(envelope(mockPortfolios[0]))),
http.delete(`${API}/portfolios/:id`, () => HttpResponse.json(envelope(null))),
http.post(`${API}/portfolios/:id/positions`, () => HttpResponse.json(envelope(mockPositions[0]))),
http.patch(`${API}/portfolios/:id/positions/:positionId`, () =>
HttpResponse.json(envelope(mockPositions[0])),
),
http.delete(`${API}/portfolios/:id/positions/:positionId`, () =>
HttpResponse.json(envelope(null)),
),
http.get(`${API}/portfolios/:id/analytics`, () => HttpResponse.json(envelope(mockAnalytics))),
http.get(`${API}/broker/accounts`, () => HttpResponse.json(envelope(mockBrokerAccounts))),
http.get(`${API}/broker/accounts/:accountId/portfolio`, () =>
HttpResponse.json(envelope(mockBrokerPortfolio)),
),
http.get(`${API}/broker/accounts/:accountId/positions`, () =>
HttpResponse.json(
envelope({
accountId: '2084014113',
items: mockBrokerPositions,
nextCursor: null,
hasNext: false,
asOf: '2024-06-01T10:00:00Z',
}),
),
),
http.get(`${API}/broker/accounts/:accountId/operations`, () =>
HttpResponse.json(
envelope({
accountId: '2084014113',
items: mockBrokerOperations,
nextCursor: null,
hasNext: false,
asOf: '2024-06-01T10:00:00Z',
}),
),
),
http.get(`${API}/broker/accounts/:accountId/events`, () =>
HttpResponse.json(
envelope({
items: mockBrokerEvents,
summary: mockEventsSummary,
asOf: '2024-06-01T10:00:00Z',
}),
),
),
http.post(`${API}/broker/accounts/:accountId/operations/sync`, () =>
HttpResponse.json(envelope({ upserted: 5 })),
),
]

View File

@ -0,0 +1,5 @@
export function envelope(data: unknown) {
return {
data: { data, meta: { fromCache: false, cachedAt: null } },
}
}

View File

@ -1,3 +1,4 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@ -5,7 +6,6 @@ import { HttpResponse, http } from 'msw'
import { useContext } from 'react'
import { describe, expect, it } from 'vitest'
import { SessionContext } from '@/entities/session'
import { server } from '@/shared/lib/test/server'
import { SessionProvider } from './SessionProvider'
const API = '/api/v1'

View File

@ -1,9 +1,9 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { HttpResponse, http } from 'msw'
import type { ReactNode } from 'react'
import { describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { useBondCandles } from './useBondCandles'
const API = '/api/v1'

View File

@ -1,10 +1,10 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { HttpResponse, http } from 'msw'
import type { ReactNode } from 'react'
import { describe, expect, it } from 'vitest'
import { useSearch } from '@/entities/search'
import { server } from '@/shared/lib/test/server'
const API = '/api/v1'

View File

@ -1,6 +1,6 @@
import { server } from '@mocks/server'
import { HttpResponse, http } from 'msw'
import { beforeEach, describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { getMe, login, logout, refresh, register, updateProfile } from './sessionApi'
import { getAccessToken, setAccessToken } from './tokenManager'

View File

@ -1,9 +1,9 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { HttpResponse, http } from 'msw'
import type { ReactNode } from 'react'
import { describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { useStockCandles } from './useStockCandles'
const API = '/api/v1'

View File

@ -1,9 +1,9 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { HttpResponse, http } from 'msw'
import type { ReactNode } from 'react'
import { describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { useStockDividends } from './useStockDividends'
const API = '/api/v1'

View File

@ -7,7 +7,7 @@ import { env } from './shared/config/env'
async function startApp() {
if (env.VITE_API_MOCK) {
const { worker } = await import('./shared/lib/test/browser')
const { worker } = await import('../mocks/browser')
await worker.start({ onUnhandledRequest: 'bypass' })
}

View File

@ -1,3 +1,4 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
createMemoryHistory,
@ -10,7 +11,6 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse, http } from 'msw'
import { describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { TestSessionProvider } from '@/shared/lib/test/TestSessionProvider'
import { LoginPage } from './ui/LoginPage'

View File

@ -9,7 +9,7 @@ import { PortfolioSummary } from '@/widgets/portfolio-summary'
import { SharePositionTable } from '@/widgets/share-positions-table'
export function PortfolioDetailPage() {
const { id } = useParams<{ id: string }>()
const { id } = useParams({ from: '/portfolios/$id' })
const portfolioId = parseInt(id!, 10)
const { data: portfolio, isLoading, error } = usePortfolio(portfolioId)

View File

@ -1,8 +1,8 @@
import { server } from '@mocks/server'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse, http } from 'msw'
import { describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { renderWithProviders } from '@/shared/lib/test/test-utils'
import { ProfilePage } from './ui/ProfilePage'

View File

@ -1,3 +1,4 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
createMemoryHistory,
@ -10,7 +11,6 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse, http } from 'msw'
import { describe, expect, it } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { TestSessionProvider } from '@/shared/lib/test/TestSessionProvider'
import { RegisterPage } from './ui/RegisterPage'

View File

@ -5,7 +5,7 @@ import { PriceChart } from '@/widgets/price-chart'
import { StockDetails } from '@/widgets/stock-details'
export function StockPage() {
const { secid } = useParams<{ secid: string }>()
const { secid } = useParams({ from: '/stocks/$secid' })
const { data: stock, isLoading, error } = useStock(secid!)
const till = new Date().toISOString().split('T')[0]
const from = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]

View File

@ -1,190 +0,0 @@
import { HttpResponse, http } from 'msw'
import type { BondResponse, CandleItem, SearchResultItem, ShareResponse } from '@/shared/api'
const API = '/api/v1'
const mockShare: ShareResponse = {
secid: 'SBER',
isin: 'RU0009029540',
name: 'Сбер Банк',
shortName: 'Сбер',
latName: 'Sberbank',
listLevel: 1,
issueSize: 21586900000,
faceValue: 3,
faceUnit: 'RUB',
type: 'common_share',
marketData: {
price: 289.5,
change: 2.5,
changePercent: 0.87,
open: 287,
high: 291,
low: 286.5,
volume: 15000000,
value: 4350000000,
issueCapitalization: 6250000000000,
updatedAt: '2024-01-15T10:00:00Z',
},
}
const mockBond: BondResponse = {
secid: 'SU26238RMFS5',
isin: 'RU000A101XU7',
name: 'ОФЗ 26238',
shortName: 'ОФЗ 26238',
latName: null,
listLevel: 1,
issueSize: 500000000,
faceValue: 1000,
faceUnit: 'RUB',
matDate: '2027-05-15',
couponValue: 36.9,
couponPercent: 7.5,
couponPeriod: 182,
nextCoupon: '2024-07-15',
accruedInt: 8.45,
bondType: 'ОФЗ',
bondSubType: 'ОФЗ-ПД',
offerDate: null,
buybackDate: null,
marketData: {
price: 98.5,
yieldToMaturity: 8.2,
duration: 3.5,
accruedInt: 8.45,
couponValue: 36.9,
couponPercent: 7.5,
nextCouponDate: '2024-07-15',
open: 98.0,
high: 99.0,
low: 97.5,
volume: 1000000,
updatedAt: '2024-01-15T10:00:00Z',
},
}
const mockCandles: CandleItem[] = [
{
open: 280,
high: 290,
low: 278,
close: 289.5,
volume: 1000000,
value: 280000000,
begin: '2024-01-15T10:00:00Z',
end: '2024-01-15T18:00:00Z',
},
{
open: 289,
high: 292,
low: 285,
close: 288,
volume: 800000,
value: 231200000,
begin: '2024-01-16T10:00:00Z',
end: '2024-01-16T18:00:00Z',
},
]
const mockDividends = [
{ registryCloseDate: '2024-07-10', value: 35.0, currency: 'RUB' },
{ registryCloseDate: '2023-10-05', value: 30.0, currency: 'RUB' },
]
const mockSearchResults: SearchResultItem[] = [
{
secid: 'SBER',
isin: 'RU0009029540',
shortName: 'Сбер',
type: 'share',
listLevel: 1,
currency: 'RUB',
price: 289.5,
},
{
secid: 'VTBR',
isin: 'RU000A0JP5V6',
shortName: 'ВТБ',
type: 'share',
listLevel: 1,
currency: 'RUB',
price: 0.0234,
},
]
const userResponse = {
id: 1,
email: 'user@test.com',
name: 'Test User',
role: 'user',
}
const authResponse = {
user: userResponse,
accessToken: 'mock-access-token',
}
const envelope = (data: unknown) => ({
data: { data, meta: { fromCache: false, cachedAt: null } },
})
export const handlers = [
http.get(`${API}/securities/search`, ({ request }) => {
const url = new URL(request.url)
const q = url.searchParams.get('q') || ''
if (q.length < 2) {
return HttpResponse.json(envelope([]))
}
const filtered = mockSearchResults.filter(
(r) =>
r.secid.toLowerCase().includes(q.toLowerCase()) ||
r.shortName.toLowerCase().includes(q.toLowerCase()),
)
return HttpResponse.json(envelope(filtered))
}),
http.get(`${API}/securities/shares/:secid`, ({ params }) => {
const { secid } = params
if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 })
return HttpResponse.json(envelope({ ...mockShare, secid } as ShareResponse))
}),
http.get(`${API}/securities/shares/:secid/candles`, () =>
HttpResponse.json(envelope(mockCandles)),
),
http.get(`${API}/securities/shares/:secid/dividends`, () =>
HttpResponse.json(envelope(mockDividends)),
),
http.get(`${API}/securities/bonds/:secid`, ({ params }) => {
const { secid } = params
if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 })
return HttpResponse.json(envelope({ ...mockBond, secid } as BondResponse))
}),
http.get(`${API}/securities/bonds/:secid/candles`, () =>
HttpResponse.json(envelope(mockCandles)),
),
http.get(`${API}/auth/me`, () => HttpResponse.json(envelope(userResponse))),
http.post(`${API}/auth/login`, () => HttpResponse.json(envelope(authResponse))),
http.post(`${API}/auth/register`, () => HttpResponse.json(envelope(authResponse))),
http.post(`${API}/auth/refresh`, () => HttpResponse.json(envelope(authResponse))),
http.post(`${API}/auth/logout`, () => HttpResponse.json(envelope({ message: 'Logged out' }))),
http.patch(`${API}/auth/me`, () =>
HttpResponse.json(envelope({ ...userResponse, name: 'Updated' })),
),
http.get(`${API}/health`, () =>
HttpResponse.json(
envelope({ status: 'ok', timestamp: new Date().toISOString(), uptime: 12345 }),
),
),
]

View File

@ -1,5 +1,5 @@
import '@testing-library/jest-dom'
import { server } from './server'
import { server } from '@mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())

View File

@ -1,10 +1,10 @@
import { server } from '@mocks/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse, http } from 'msw'
import type { ReactNode } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { server } from '@/shared/lib/test/server'
import { SearchBar } from '@/widgets/search-bar'
const API = '/api/v1'

View File

@ -17,10 +17,11 @@
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@mocks/*": ["./mocks/*"]
}
},
"include": ["src"],
"include": ["src", "mocks"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/shared/lib/test/**"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -5,9 +5,10 @@ import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
alias: [
{ find: '@mocks', replacement: path.resolve(__dirname, './mocks') },
{ find: '@', replacement: path.resolve(__dirname, './src') },
],
},
server: {
port: 5173,

View File

@ -6,9 +6,10 @@ export default defineConfig({
plugins: [react()],
logLevel: 'error',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
alias: [
{ find: '@mocks', replacement: path.resolve(__dirname, './mocks') },
{ find: '@', replacement: path.resolve(__dirname, './src') },
],
},
test: {
environment: 'jsdom',