feat: add Storybook for WealthLedger components

This commit is contained in:
2026-09-07 22:22:37 +03:00
parent 9e42148f6a
commit e63fd89a39
18 changed files with 2889 additions and 22 deletions
+1
View File
@@ -9,6 +9,7 @@ lerna-debug.log*
node_modules node_modules
dist dist
storybook-static
dist-ssr dist-ssr
coverage coverage
*.local *.local
+18
View File
@@ -0,0 +1,18 @@
const base = require("../dependency-cruiser.cjs");
module.exports = {
...base,
forbidden: [
...base.forbidden,
{
name: "stories-must-not-reach-application-data",
severity: "error",
from: { path: "\\.stories\\.tsx$|^\\.storybook/preview\\.tsx$" },
to: {
path: "^src/(shared/api|features/auth|app/routes|app/ui)/|^src/(entities|features)/[^/]+/api/|\\.vm\\.ts$|^src/pages/[^/]+/ui/[^/]+Page\\.tsx$|node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/",
reachable: true,
},
},
],
options: { ...base.options, tsPreCompilationDeps: false },
};
+10
View File
@@ -0,0 +1,10 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.tsx"],
addons: ["@storybook/addon-docs"],
framework: "@storybook/react-vite",
core: { disableTelemetry: true },
};
export default config;
+36
View File
@@ -0,0 +1,36 @@
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "dayjs/locale/ru";
import "../src/app/styles.css";
import "../src/shared/i18n";
import { MantineProvider } from "@mantine/core";
import type { Preview } from "@storybook/react-vite";
import { theme } from "../src/app/theme";
const colorSchemeManager = {
get: () => "light" as const,
set: () => {},
clear: () => {},
subscribe: () => {},
unsubscribe: () => {},
};
const preview: Preview = {
parameters: {
layout: "padded",
controls: { expanded: true },
},
decorators: [
(Story) => (
<MantineProvider
theme={theme}
forceColorScheme="light"
colorSchemeManager={colorSchemeManager}
>
<Story />
</MantineProvider>
),
],
};
export default preview;
+18 -1
View File
@@ -7,6 +7,7 @@
"includes": [ "includes": [
"**", "**",
"!dist", "!dist",
"!storybook-static",
"!src/shared/api/generated", "!src/shared/api/generated",
"!src/shared/api/spec", "!src/shared/api/spec",
"!coverage", "!coverage",
@@ -32,7 +33,12 @@
}, },
"overrides": [ "overrides": [
{ {
"includes": ["vite.config.ts", "vitest.config.ts"], "includes": [
"vite.config.ts",
"vitest.config.ts",
".storybook/main.ts",
".storybook/preview.tsx"
],
"linter": { "linter": {
"rules": { "rules": {
"style": { "style": {
@@ -40,6 +46,17 @@
} }
} }
} }
},
{
"includes": ["**/*.stories.tsx"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off",
"useExportsLast": "off"
}
}
}
} }
] ]
} }
+7
View File
@@ -20,6 +20,13 @@ Biome-enforced guardrails:
- Не использовать default exports. - Не использовать default exports.
- Export statements держать после declarations тела module. - Export statements держать после declarations тела module.
Storybook CSF-файлы `*.stories.tsx` используют штатный `export default meta`
и прямые `export const` для stories: статический indexer должен видеть их
метаданные и названия. Biome разрешает эти два исключения только для stories;
`.storybook/main.ts` и `preview.tsx` разрешают только обязательный default export.
Synthetic fixtures и Storybook descriptions являются данными/документацией
истории; продуктовые UI-строки по-прежнему принадлежат i18n.
Mandatory review conventions: Mandatory review conventions:
- React components типизировать через `FC`: `const Component: FC<Props> = (props) => { ... }`; если props нет — `const Component: FC = () => { ... }`. - React components типизировать через `FC`: `const Component: FC<Props> = (props) => { ... }`; если props нет — `const Component: FC = () => { ... }`.
+2190 -11
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -8,6 +8,9 @@
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:catalog": "storybook dev -p 6006 --no-open --disable-telemetry",
"build:catalog": "storybook build --output-dir storybook-static --disable-telemetry",
"preview:catalog": "vite preview --outDir storybook-static",
"typecheck": "tsc -b --noEmit", "typecheck": "tsc -b --noEmit",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "biome check .", "lint": "biome check .",
@@ -18,9 +21,10 @@
"openapi:check": "node scripts/check-openapi.mjs", "openapi:check": "node scripts/check-openapi.mjs",
"knip": "knip", "knip": "knip",
"deps": "depcruise src --config dependency-cruiser.cjs --output-type err-long", "deps": "depcruise src --config dependency-cruiser.cjs --output-type err-long",
"verify": "npm run openapi:check && npm run typecheck && npm run lint && npm run test:coverage && npm run knip && npm run deps && npm run build", "verify": "npm run openapi:check && npm run typecheck && npm run lint && npm run test:coverage && npm run knip && npm run deps && npm run deps:storybook && npm run build && npm run build:catalog",
"prepare": "husky", "prepare": "husky",
"preview": "vite preview" "preview": "vite preview",
"deps:storybook": "depcruise src .storybook/preview.tsx --config .storybook/dependency-cruiser.cjs --output-type err-long"
}, },
"lint-staged": { "lint-staged": {
"*.{css,html,json,md,ts,tsx}": "biome format --write" "*.{css,html,json,md,ts,tsx}": "biome format --write"
@@ -47,6 +51,8 @@
"@commitlint/cli": "21.2.2", "@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2", "@commitlint/config-conventional": "21.2.2",
"@rolldown/plugin-babel": "0.2.3", "@rolldown/plugin-babel": "0.2.3",
"@storybook/addon-docs": "10.6.0",
"@storybook/react-vite": "10.6.0",
"@testing-library/jest-dom": "6.9.1", "@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.0", "@testing-library/react": "16.3.0",
"@types/node": "24.13.3", "@types/node": "24.13.3",
@@ -62,6 +68,7 @@
"lint-staged": "16.2.7", "lint-staged": "16.2.7",
"msw": "2.15.0", "msw": "2.15.0",
"openapi-typescript": "7.13.0", "openapi-typescript": "7.13.0",
"storybook": "10.6.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"vite": "8.2.0", "vite": "8.2.0",
"vitest": "4.1.10" "vitest": "4.1.10"
@@ -0,0 +1,115 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MemoryRouter } from "react-router";
import type { BrokerageAccount } from "../api/useBrokerageAccounts";
import { brokerageAccountCards } from "../model/display";
import { BrokerageAccountList } from "./BrokerageAccountList";
const syntheticAccount = {
id: "storybook-account",
name: "Учебный брокерский счёт",
type: "BROKERAGE",
openedOn: "2025-03-15",
portfolioSummary: {
totalsByCurrency: [
{ currency: "RUB", value: "125000.50" },
{ currency: "USD", value: "320" },
{ currency: "EUR", value: "210" },
],
},
} satisfies BrokerageAccount;
const meta = {
title: "WealthLedger/BrokerageAccountList",
component: BrokerageAccountList,
tags: ["autodocs"],
argTypes: {
accounts: { control: false },
},
decorators: [
(Story) => (
<MemoryRouter initialEntries={["/accounts"]}>
<Story />
</MemoryRouter>
),
],
parameters: {
docs: {
description: {
component:
"Карточки счетов WealthLedger со сводкой по валютам. Используйте на экране выбора счёта; отсутствие сводки и отсутствие сумм имеют разные сообщения. Примеры содержат вымышленные данные. Ссылки меняют только локальный маршрут MemoryRouter; страница назначения в этой истории не отображается.",
},
},
},
args: { accounts: brokerageAccountCards([syntheticAccount]) },
} satisfies Meta<typeof BrokerageAccountList>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Available: Story = {
name: "Сводка по нескольким валютам",
parameters: {
docs: {
description: {
story:
"Показываются первые две валюты и количество остальных. Суммы разных валют не складываются в общий итог.",
},
},
},
};
export const Unavailable: Story = {
name: "Сводка недоступна",
args: {
accounts: brokerageAccountCards([
{ ...syntheticAccount, portfolioSummary: null, openedOn: null },
]),
},
parameters: {
docs: {
description: {
story:
"Счёт остаётся доступен для открытия, даже если сводка временно недоступна. Неизвестные суммы не заменяются нулём; отсутствующая дата открытия не отображается.",
},
},
},
};
export const Empty: Story = {
name: "Нет текущих сумм",
args: {
accounts: brokerageAccountCards([
{ ...syntheticAccount, portfolioSummary: { totalsByCurrency: [] } },
]),
},
parameters: {
docs: {
description: {
story:
"Успешно полученная пустая сводка отличается от ошибки получения данных. Карточка сохраняет название, тип счёта и переход.",
},
},
},
};
export const LongName: Story = {
name: "Длинное название счёта",
args: {
accounts: brokerageAccountCards([
{
...syntheticAccount,
name: "Учебный индивидуальный инвестиционный счёт для долгосрочных накоплений",
type: "INDIVIDUAL_INVESTMENT",
},
]),
},
parameters: {
docs: {
description: {
story:
"Проверьте перенос длинного названия на узком экране и доступность всей карточки с клавиатуры.",
},
},
},
};
@@ -0,0 +1,21 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AccountPageLoading } from "./AccountPageLoading.view";
const meta = {
title: "WealthLedger/AccountPageLoading",
component: AccountPageLoading,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component:
"Начальная загрузка страницы счёта без доступных данных: настоящий page-owned skeleton заголовка, сводки и строк. Shared LoadingSkeleton предоставляет busy-region, фигуры декоративны и не получают фокус. Не используйте вместо сохранённых данных при refetch; запросы в витрине не выполняются.",
},
},
},
} satisfies Meta<typeof AccountPageLoading>;
export default meta;
type Story = StoryObj<typeof meta>;
export const InitialLoading: Story = { name: "Начальная загрузка счёта" };
@@ -0,0 +1,49 @@
import { Stack } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { PortfolioRefreshState } from "./PortfolioRefreshState.view";
import { PortfolioTotals } from "./PortfolioTotals.view";
const meta = {
title: "WealthLedger/PortfolioRefreshState",
component: PortfolioRefreshState,
tags: ["autodocs"],
args: { refreshing: false, refreshError: false, retry: fn() },
argTypes: {
refreshing: { control: "boolean" },
refreshError: { control: "boolean" },
retry: { control: false },
},
parameters: {
docs: {
description: {
component:
"Обновление уже показанных данных портфеля. Реальные PortfolioTotals остаются видимыми вместе с PortfolioRefreshState при refetch и его ошибке. Суммы синтетические; это presentation Client Cached Snapshot, без QueryClient и запроса API. Retry регистрирует локальное действие в Actions и не доказывает восстановление API.",
},
},
},
render: (args) => (
<Stack>
<PortfolioTotals
totals={[
{ currency: "RUB", value: "125000.50" },
{ currency: "USD", value: "320" },
]}
/>
<PortfolioRefreshState {...args} />
</Stack>
),
} satisfies Meta<typeof PortfolioRefreshState>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Idle: Story = { name: "Сохранённые данные" };
export const Refreshing: Story = {
name: "Фоновое обновление",
args: { refreshing: true },
};
export const RefreshError: Story = {
name: "Ошибка обновления с retry",
args: { refreshError: true },
};
@@ -0,0 +1,134 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { AccountPagePosition } from "../AccountPage.vm";
import { PositionCard } from "./PositionCard.view";
const syntheticPosition = {
figi: null,
displayName: "Учебные технологии",
type: "STOCK",
quantity: "100",
currentValue: { currency: "RUB", value: "25000" },
averagePurchasePrice: { currency: "RUB", value: "200" },
absoluteReturn: { currency: "RUB", value: "5000" },
isin: null,
ticker: "DEMO",
logoName: null,
shareText: "20%",
returnText: "+5 000 ₽",
returnTone: "positive",
logoSrc: null,
} satisfies AccountPagePosition;
const meta = {
title: "WealthLedger/PositionCard",
component: PositionCard,
tags: ["autodocs"],
argTypes: {
kind: {
control: "inline-radio",
options: ["stocks", "bonds", "other"],
},
position: { control: false },
},
parameters: {
docs: {
description: {
component:
"Позиция портфеля WealthLedger: количество, стоимость, результат и доля. Доля и текст результата поступают готовыми из модели представления. Карточка не рассчитывает доходность. Все данные вымышлены; логотип заменён буквенным обозначением.",
},
},
},
args: { kind: "stocks", position: syntheticPosition },
} satisfies Meta<typeof PositionCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const StockGain: Story = {
name: "Акция с положительным результатом",
parameters: {
docs: {
description: {
story:
"Развёрнутая карточка акции показывает тикер, оценку позиции и положительный результат. Цвет дополняет знак суммы.",
},
},
},
};
export const BondLoss: Story = {
name: "Облигация с отрицательным результатом",
args: {
kind: "bonds",
position: {
...syntheticPosition,
displayName: "Учебные облигации для долгосрочных накоплений, выпуск 2030",
type: "BOND",
quantity: "10",
currentValue: { currency: "RUB", value: "9500" },
averagePurchasePrice: { currency: "RUB", value: "1000" },
absoluteReturn: { currency: "RUB", value: "-500" },
isin: "DEMO-BOND-2030",
ticker: null,
shareText: "7,6%",
returnText: "−500 ₽",
returnTone: "negative",
},
},
parameters: {
docs: {
description: {
story:
"Для облигации используется идентификатор выпуска. Длинное имя и отрицательная сумма позволяют проверить компактное представление на узком экране.",
},
},
},
};
export const Other: Story = {
name: "Прочий инструмент",
args: {
kind: "other",
position: {
...syntheticPosition,
displayName: "Учебный фонд",
type: "FUND",
isin: "DEMO-FUND",
ticker: null,
},
},
parameters: {
docs: {
description: {
story:
"Компактный вариант для остальных инструментов сохраняет количество, стоимость и пояснение результата без оформления акции или облигации.",
},
},
},
};
export const MissingValuation: Story = {
name: "Оценка и идентификаторы отсутствуют",
args: {
position: {
...syntheticPosition,
displayName: "Учебная позиция без текущей оценки",
currentValue: null,
averagePurchasePrice: null,
absoluteReturn: null,
ticker: null,
shareText: null,
returnText: null,
returnTone: undefined,
},
},
parameters: {
docs: {
description: {
story:
"При отсутствии оценки остаются название и количество. Неизвестная стоимость, доля и результат не изображаются нулевыми значениями.",
},
},
},
};
@@ -0,0 +1,133 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MemoryRouter } from "react-router";
import type { BrokerageEvent } from "@/entities/brokerage-event";
import { RecentEvents } from "./RecentEvents.view";
const syntheticEvent = {
type: "coupon",
date: "2026-10-15",
dateKind: "event",
figi: null,
instrumentUid: null,
ticker: "DEMO",
displayName: "Учебные облигации",
projectedAmount: { currency: "RUB", value: "850.50" },
} satisfies BrokerageEvent;
const eventsPath = "/accounts/storybook-account/events";
const meta = {
title: "WealthLedger/RecentEvents",
component: RecentEvents,
tags: ["autodocs"],
argTypes: {
events: { control: false },
},
decorators: [
(Story) => (
<MemoryRouter initialEntries={["/accounts/storybook-account"]}>
<Story />
</MemoryRouter>
),
],
parameters: {
docs: {
description: {
component:
"Раздел ближайших событий на странице счёта WealthLedger. Раздел самостоятельно отображает загрузку, ошибку, отсутствие событий и полученные данные. Все события и суммы вымышлены. Ссылка на календарь меняет только локальный маршрут MemoryRouter; сам календарь в этой истории не отображается.",
},
},
},
args: {
events: {
state: "loaded",
to: eventsPath,
items: [
syntheticEvent,
{
...syntheticEvent,
type: "dividend",
date: "2026-11-01",
dateKind: "registry",
ticker: "EXAMPLE",
displayName: "Учебные технологии",
projectedAmount: { currency: "RUB", value: "1200" },
},
],
},
},
} satisfies Meta<typeof RecentEvents>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Loaded: Story = {
name: "События получены",
parameters: {
docs: {
description: {
story:
"Предстоящие купоны и дивиденды показываются с ожидаемой суммой. Это прогноз событий, а не подтверждение получения дохода.",
},
},
},
};
export const Loading: Story = {
name: "Загрузка раздела",
args: { events: { state: "loading", to: eventsPath } },
parameters: {
docs: {
description: {
story:
"Локальный skeleton занимает место строк событий и имеет доступное обозначение загрузки. Остальные разделы страницы могут оставаться доступными.",
},
},
},
};
export const Failed: Story = {
name: "Ошибка получения событий",
args: { events: { state: "error", to: eventsPath } },
parameters: {
docs: {
description: {
story:
"Ошибка ограничена разделом событий. Сохраняется переход к полному календарю; недоступные события не выдаются за пустой список.",
},
},
},
};
export const Empty: Story = {
name: "Нет ближайших событий",
args: { events: { state: "empty", to: eventsPath } },
parameters: {
docs: {
description: {
story:
"Успешно полученный пустой результат объясняется отдельным сообщением и сохраняет переход к календарю.",
},
},
},
};
export const MissingAmount: Story = {
name: "Событие без ожидаемой суммы",
args: {
events: {
state: "loaded",
to: eventsPath,
items: [{ ...syntheticEvent, projectedAmount: null }],
},
},
parameters: {
docs: {
description: {
story:
"Событие остаётся видимым при неизвестной ожидаемой сумме. Отсутствие оценки не подменяется нулевой выплатой.",
},
},
},
};
+78
View File
@@ -0,0 +1,78 @@
import { Button } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ComponentProps } from "react";
import { fn } from "storybook/test";
import { StateView } from "./StateView";
type StateStoryArgs = ComponentProps<typeof StateView> & {
withAction: boolean;
onAction: () => void;
};
const meta = {
title: "WealthLedger/StateView",
component: StateView,
tags: ["autodocs"],
args: {
title: "Нет учебных данных",
text: "Синтетическое пустое состояние. Пользовательские данные не загружаются.",
error: false,
withAction: false,
onAction: fn(),
},
argTypes: {
title: { control: "text" },
text: { control: "text" },
error: { control: "boolean" },
action: { table: { disable: true } },
withAction: {
control: "boolean",
description: "Показать необязательное действие StateView",
},
onAction: { control: false },
},
parameters: {
docs: {
description: {
component:
"Общее empty/error состояние WealthLedger с необязательным действием. Error сохраняет alert semantics. Для initial loading используйте skeleton страницы, для refetch — локальное предупреждение рядом с сохранёнными данными. Кнопка вызывает только Actions; API recovery и полный пользовательский сценарий здесь не проверяются.",
},
},
},
render: (args) => {
const { withAction, onAction, ...stateProps } = args;
return (
<StateView
{...stateProps}
action={
withAction ? (
<Button onClick={onAction} variant="default">
{args.error ? "Повторить локально" : "Выполнить пример"}
</Button>
) : undefined
}
/>
);
},
} satisfies Meta<StateStoryArgs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Empty: Story = { name: "Пустое состояние" };
export const EmptyWithAction: Story = {
name: "Пустое состояние с действием",
args: { withAction: true },
};
export const Failed: Story = {
name: "Ошибка",
args: {
error: true,
title: "Учебная ошибка",
text: "Демонстрация недоступных данных без вызова API.",
},
};
export const ErrorWithAction: Story = {
name: "Ошибка с retry",
args: { ...Failed.args, withAction: true },
};
+60
View File
@@ -0,0 +1,60 @@
import { composeStories } from "@storybook/react-vite";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { fn } from "storybook/test";
import { afterEach, describe, expect, it, vi } from "vitest";
import preview from "../../.storybook/preview";
import * as AccountStories from "../entities/brokerage-account/ui/BrokerageAccountList.stories";
import * as LoadingStories from "../pages/account/ui/components/AccountPageLoading.stories";
import * as RefreshStories from "../pages/account/ui/components/PortfolioRefreshState.stories";
import * as PositionStories from "../pages/account/ui/components/PositionCard.stories";
import * as EventStories from "../pages/account/ui/components/RecentEvents.stories";
import * as StateStories from "../shared/ui/StateView.stories";
afterEach(() => vi.restoreAllMocks());
describe("Storybook isolated product examples", () => {
it("renders every fixture and runs callbacks without application network or browser data", () => {
const spies = [
vi.spyOn(globalThis, "fetch"),
vi.spyOn(XMLHttpRequest.prototype, "open"),
vi.spyOn(Storage.prototype, "getItem"),
vi.spyOn(Storage.prototype, "setItem"),
vi.spyOn(Storage.prototype, "removeItem"),
vi.spyOn(Document.prototype, "cookie", "get"),
vi.spyOn(Document.prototype, "cookie", "set"),
];
const stories = [
...Object.values(composeStories(AccountStories, preview)),
...Object.values(composeStories(LoadingStories, preview)),
...Object.values(composeStories(RefreshStories, preview)),
...Object.values(composeStories(PositionStories, preview)),
...Object.values(composeStories(EventStories, preview)),
...Object.values(composeStories(StateStories, preview)),
];
for (const Story of stories) {
render(<Story />);
for (const button of screen.queryAllByRole("button"))
fireEvent.click(button);
for (const link of screen.queryAllByRole("link")) fireEvent.click(link);
for (const image of screen.queryAllByRole("img")) {
expect(image).not.toHaveAttribute("src");
}
cleanup();
}
for (const spy of spies) expect(spy).not.toHaveBeenCalled();
});
it("keeps synthetic totals visible while the refresh example calls a local retry", () => {
const { RefreshError } = composeStories(RefreshStories, preview);
const retry = fn();
const view = render(<RefreshError retry={retry} />);
expect(screen.getByText(/125\s*000,50/)).toBeVisible();
fireEvent.click(screen.getByRole("button"));
expect(retry).toHaveBeenCalledOnce();
view.rerender(
<RefreshError refreshing refreshError={false} retry={retry} />,
);
expect(screen.getByText(/125\s*000,50/)).toBeVisible();
expect(screen.getByRole("status")).toBeVisible();
});
});
+4 -6
View File
@@ -5,25 +5,23 @@
"lib": ["ES2023", "DOM"], "lib": ["ES2023", "DOM"],
"module": "esnext", "module": "esnext",
"baseUrl": ".", "baseUrl": ".",
"paths": { "@/*": ["src/*"] }, "paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"], "types": ["vite/client"],
"allowArbitraryExtensions": true, "allowArbitraryExtensions": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
/* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["src"] "include": ["src", ".storybook/preview.tsx"]
} }
+1 -1
View File
@@ -20,5 +20,5 @@
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts", ".storybook/main.ts"]
} }
+5 -1
View File
@@ -12,7 +12,11 @@ export default defineConfig({
coverage: { coverage: {
all: true, all: true,
include: ["src/**/*.{ts,tsx}"], include: ["src/**/*.{ts,tsx}"],
exclude: ["src/test/**", "src/**/*.test.{ts,tsx}"], exclude: [
"src/test/**",
"src/**/*.test.{ts,tsx}",
"src/**/*.stories.tsx",
],
reporter: ["text", "html"], reporter: ["text", "html"],
thresholds: { thresholds: {
statements: 82, statements: 82,