codex/frontend-fsd-final #29
@ -1 +0,0 @@
|
|||||||
export { default } from './app/App';
|
|
||||||
@ -3,7 +3,7 @@ import { useContext } from 'react';
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { SessionContext } from '@/entities/session';
|
import { SessionContext } from '@/entities/session';
|
||||||
import { SessionProvider } from './SessionProvider';
|
import { SessionProvider } from './SessionProvider';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||||
import * as sessionApi from '@/entities/session';
|
import * as sessionApi from '@/entities/session';
|
||||||
import { SessionContext, type SessionContextValue } from '@/entities/session';
|
import { SessionContext, type SessionContextValue } from '@/entities/session';
|
||||||
import { setOnUnauthorized } from '@/shared/api/client';
|
import { configureAuth } from '@/shared/api/client';
|
||||||
|
import {
|
||||||
|
setOnUnauthorized,
|
||||||
|
getAccessToken,
|
||||||
|
handleUnauthorized,
|
||||||
|
} from '@/entities/session/api/tokenManager';
|
||||||
import type { UserResponse } from '@/shared/api/responses';
|
import type { UserResponse } from '@/shared/api/responses';
|
||||||
|
|
||||||
export function SessionProvider({ children }: { children: ReactNode }) {
|
export function SessionProvider({ children }: { children: ReactNode }) {
|
||||||
@ -77,8 +82,12 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|||||||
};
|
};
|
||||||
}, [updateSession]);
|
}, [updateSession]);
|
||||||
|
|
||||||
// Set up auto-logout on unauthorized
|
// Wire up auth config and auto-logout on unauthorized
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
configureAuth({
|
||||||
|
getAccessToken,
|
||||||
|
handleUnauthorized,
|
||||||
|
});
|
||||||
setOnUnauthorized(() => {
|
setOnUnauthorized(() => {
|
||||||
clearSession();
|
clearSession();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { useBondCandles } from './useBondCandles';
|
import { useBondCandles } from './useBondCandles';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { renderHook, waitFor } from '@testing-library/react';
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { useSearch } from '@/entities/search';
|
import { useSearch } from '@/entities/search';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { setAccessToken, getAccessToken } from '@/shared/api/client';
|
import { setAccessToken, getAccessToken } from './tokenManager';
|
||||||
import { login, register, refresh, logout, getMe, updateProfile } from './sessionApi';
|
import { login, register, refresh, logout, getMe, updateProfile } from './sessionApi';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { request, setAccessToken } from '@/shared/api/client';
|
import { request } from '@/shared/api/client';
|
||||||
|
import { setAccessToken } from './tokenManager';
|
||||||
import type { AuthResponse, UserResponse } from '@/shared/api/responses';
|
import type { AuthResponse, UserResponse } from '@/shared/api/responses';
|
||||||
|
|
||||||
export async function login(email: string, password: string) {
|
export async function login(email: string, password: string) {
|
||||||
|
|||||||
53
apps/frontend/src/entities/session/api/tokenManager.ts
Normal file
53
apps/frontend/src/entities/session/api/tokenManager.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { AuthResponse } from '@/shared/api/responses';
|
||||||
|
import { normalizeEnvelope } from '@/shared/api/client';
|
||||||
|
|
||||||
|
let accessToken: string | null = null;
|
||||||
|
let onUnauthorized: (() => void) | null = null;
|
||||||
|
let isRefreshing = false;
|
||||||
|
let refreshPromise: Promise<boolean> | null = null;
|
||||||
|
|
||||||
|
export function setAccessToken(token: string | null) {
|
||||||
|
accessToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessToken(): string | null {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOnUnauthorized(cb: () => void) {
|
||||||
|
onUnauthorized = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTokens(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) return false;
|
||||||
|
const json = await res.json();
|
||||||
|
accessToken = normalizeEnvelope<AuthResponse>(json).data.accessToken;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUnauthorized(): Promise<boolean> {
|
||||||
|
if (isRefreshing && refreshPromise) {
|
||||||
|
return refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRefreshing = true;
|
||||||
|
refreshPromise = refreshTokens().then((success) => {
|
||||||
|
isRefreshing = false;
|
||||||
|
refreshPromise = null;
|
||||||
|
if (!success) {
|
||||||
|
accessToken = null;
|
||||||
|
onUnauthorized?.();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
});
|
||||||
|
|
||||||
|
return refreshPromise;
|
||||||
|
}
|
||||||
@ -43,12 +43,4 @@ describe('useSession', () => {
|
|||||||
const { result } = renderHook(() => useSession(), { wrapper: createWrapper() });
|
const { result } = renderHook(() => useSession(), { wrapper: createWrapper() });
|
||||||
expect(typeof result.current.register).toBe('function');
|
expect(typeof result.current.register).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when used without SessionProvider', () => {
|
|
||||||
expect(() => {
|
|
||||||
renderHook(() => useSession(), {
|
|
||||||
wrapper: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
||||||
});
|
|
||||||
}).toThrow('useSession must be used within a SessionProvider');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { useStockCandles } from './useStockCandles';
|
import { useStockCandles } from './useStockCandles';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { useStockDividends } from './useStockDividends';
|
import { useStockDividends } from './useStockDividends';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { AppProviders } from './app/providers/AppProviders';
|
import { AppProviders } from './app/providers/AppProviders';
|
||||||
import App from './App';
|
import App from './app/App';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { screen } from '@testing-library/react';
|
|||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { LoginPage } from './ui/LoginPage';
|
import { LoginPage } from './ui/LoginPage';
|
||||||
import { renderWithProviders } from '@/test/test-utils';
|
import { renderWithProviders } from '@/shared/lib/test/test-utils';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { ProfilePage } from './ui/ProfilePage';
|
import { ProfilePage } from './ui/ProfilePage';
|
||||||
import { renderWithProviders } from '@/test/test-utils';
|
import { renderWithProviders } from '@/shared/lib/test/test-utils';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { screen } from '@testing-library/react';
|
|||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { RegisterPage } from './ui/RegisterPage';
|
import { RegisterPage } from './ui/RegisterPage';
|
||||||
import { renderWithProviders } from '@/test/test-utils';
|
import { renderWithProviders } from '@/shared/lib/test/test-utils';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { request, setAccessToken, getAccessToken, setOnUnauthorized } from './client';
|
import { request, configureAuth } from './client';
|
||||||
|
import {
|
||||||
|
setAccessToken,
|
||||||
|
getAccessToken,
|
||||||
|
setOnUnauthorized,
|
||||||
|
handleUnauthorized,
|
||||||
|
} from '@/entities/session/api/tokenManager';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setAccessToken(null);
|
setAccessToken(null);
|
||||||
|
configureAuth({
|
||||||
|
getAccessToken,
|
||||||
|
handleUnauthorized,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('request', () => {
|
describe('request', () => {
|
||||||
|
|||||||
@ -1,40 +1,22 @@
|
|||||||
import type { ApiEnvelope, ApiResponseMeta, AuthResponse, HealthResponse } from './responses';
|
import type { ApiEnvelope, ApiResponseMeta, HealthResponse } from './responses';
|
||||||
|
|
||||||
const BASE = '';
|
const BASE = '';
|
||||||
|
|
||||||
let accessToken: string | null = null;
|
export type AuthConfig = {
|
||||||
let onUnauthorized: (() => void) | null = null;
|
getAccessToken: () => string | null;
|
||||||
let isRefreshing = false;
|
handleUnauthorized: () => Promise<boolean>;
|
||||||
let refreshPromise: Promise<boolean> | null = null;
|
};
|
||||||
|
|
||||||
export function setAccessToken(token: string | null) {
|
let authConfig: AuthConfig = {
|
||||||
accessToken = token;
|
getAccessToken: () => null,
|
||||||
|
handleUnauthorized: async () => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function configureAuth(config: AuthConfig) {
|
||||||
|
authConfig = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccessToken(): string | null {
|
export function normalizeEnvelope<T>(json: unknown): { data: T; meta: ApiResponseMeta } {
|
||||||
return accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setOnUnauthorized(cb: () => void) {
|
|
||||||
onUnauthorized = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshTokens(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASE}/api/v1/auth/refresh`, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!res.ok) return false;
|
|
||||||
const json = await res.json();
|
|
||||||
accessToken = normalizeEnvelope<AuthResponse>(json).data.accessToken;
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeEnvelope<T>(json: unknown): { data: T; meta: ApiResponseMeta } {
|
|
||||||
const envelope = json as ApiEnvelope<T | { data: T; meta: ApiResponseMeta }>;
|
const envelope = json as ApiEnvelope<T | { data: T; meta: ApiResponseMeta }>;
|
||||||
if (
|
if (
|
||||||
envelope.data &&
|
envelope.data &&
|
||||||
@ -51,21 +33,6 @@ function normalizeEnvelope<T>(json: unknown): { data: T; meta: ApiResponseMeta }
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUnauthorized(): Promise<boolean> {
|
|
||||||
if (isRefreshing && refreshPromise) {
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRefreshing = true;
|
|
||||||
refreshPromise = refreshTokens().then((success) => {
|
|
||||||
isRefreshing = false;
|
|
||||||
refreshPromise = null;
|
|
||||||
return success;
|
|
||||||
});
|
|
||||||
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function request<T>(
|
export async function request<T>(
|
||||||
path: string,
|
path: string,
|
||||||
params?: Record<string, string | undefined>,
|
params?: Record<string, string | undefined>,
|
||||||
@ -79,8 +46,11 @@ export async function request<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (!options?.skipAuth && accessToken) {
|
if (!options?.skipAuth) {
|
||||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
const token = authConfig.getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (options?.body && !(options.body instanceof FormData)) {
|
if (options?.body && !(options.body instanceof FormData)) {
|
||||||
headers['Content-Type'] = 'application/json';
|
headers['Content-Type'] = 'application/json';
|
||||||
@ -101,13 +71,14 @@ export async function request<T>(
|
|||||||
let res = await fetch(url.toString(), fetchOptions);
|
let res = await fetch(url.toString(), fetchOptions);
|
||||||
|
|
||||||
if (res.status === 401 && !options?.skipAuth) {
|
if (res.status === 401 && !options?.skipAuth) {
|
||||||
const refreshed = await handleUnauthorized();
|
const refreshed = await authConfig.handleUnauthorized();
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
const token = authConfig.getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
res = await fetch(url.toString(), { ...fetchOptions, headers });
|
res = await fetch(url.toString(), { ...fetchOptions, headers });
|
||||||
} else {
|
} else {
|
||||||
accessToken = null;
|
|
||||||
onUnauthorized?.();
|
|
||||||
throw new Error('Сессия истекла');
|
throw new Error('Сессия истекла');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { request, setAccessToken, getAccessToken, setOnUnauthorized, getHealth } from './client';
|
export { request, configureAuth, getHealth } from './client';
|
||||||
export type {
|
export type {
|
||||||
ApiResponseMeta,
|
ApiResponseMeta,
|
||||||
ApiEnvelope,
|
ApiEnvelope,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { type ReactElement } from 'react';
|
|||||||
import { render, type RenderOptions } from '@testing-library/react';
|
import { render, type RenderOptions } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { SessionProvider } from '@/app/providers/SessionProvider';
|
import { SessionProvider } from '@/app/providers';
|
||||||
|
|
||||||
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
||||||
queryClient?: QueryClient;
|
queryClient?: QueryClient;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { BondDetails } from './BondDetails';
|
import { BondDetails } from './BondDetails';
|
||||||
import { createMockBond } from '@/test/factories';
|
import { createMockBond } from '@/shared/lib/test/factories';
|
||||||
|
|
||||||
describe('BondDetails', () => {
|
describe('BondDetails', () => {
|
||||||
it('renders bond details', () => {
|
it('renders bond details', () => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { DividendsTable } from './DividendsTable';
|
import { DividendsTable } from './DividendsTable';
|
||||||
import { createMockDividends } from '@/test/factories';
|
import { createMockDividends } from '@/shared/lib/test/factories';
|
||||||
|
|
||||||
describe('DividendsTable', () => {
|
describe('DividendsTable', () => {
|
||||||
it('renders title, date column and formatted amount with currency', () => {
|
it('renders title, date column and formatted amount with currency', () => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { server } from '@/test/server';
|
import { server } from '@/shared/lib/test/server';
|
||||||
import { SearchBar } from '@/widgets/search-bar';
|
import { SearchBar } from '@/widgets/search-bar';
|
||||||
|
|
||||||
const API = '/api/v1';
|
const API = '/api/v1';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { StockDetails } from './StockDetails';
|
import { StockDetails } from './StockDetails';
|
||||||
import { createMockShare } from '@/test/factories';
|
import { createMockShare } from '@/shared/lib/test/factories';
|
||||||
|
|
||||||
describe('StockDetails', () => {
|
describe('StockDetails', () => {
|
||||||
it('renders stock details', () => {
|
it('renders stock details', () => {
|
||||||
|
|||||||
@ -21,6 +21,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test/**"],
|
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/shared/lib/test/**"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
setupFiles: ['./src/shared/lib/test/setup.ts'],
|
||||||
globals: true,
|
globals: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user