refactor: colocate frontend component files
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Successful in 5m10s
Conventional commits / commits (pull_request) Successful in 25s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 5m10s

This commit is contained in:
2026-09-23 08:34:58 +03:00
parent 85fcfe3d07
commit b987fd8e78
190 changed files with 131 additions and 108 deletions
@@ -0,0 +1,111 @@
import { QueryClientProvider } from "@tanstack/react-query";
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { HttpResponse, http } from "msw";
import type { FC } from "react";
import { MemoryRouter, useNavigate } from "react-router";
import { beforeEach, describe, expect, it } from "vitest";
import { AuthProvider } from "@/features/auth";
import { createTestQueryClient } from "@/test/query";
import { server } from "@/test/server";
import { ProtectedDocumentTitle } from "./DocumentTitle";
const Navigation: FC = () => {
const navigate = useNavigate();
return (
<>
<button type="button" onClick={() => navigate("/accounts/all/income")}>
Все счета
</button>
<button type="button" onClick={() => navigate(-1)}>
Назад
</button>
</>
);
};
const renderTitle = (path: string) =>
render(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[path]}>
<AuthProvider>
<ProtectedDocumentTitle />
<Navigation />
</AuthProvider>
</MemoryRouter>
</QueryClientProvider>,
);
beforeEach(() => {
document.title = "Previous page";
server.use(
http.post("*/api/v1/auth/refresh", () =>
HttpResponse.json({
accessToken: "test-token",
user: { id: "user-1", email: "user@example.com" },
}),
),
);
});
describe("document titles", () => {
it("updates a loading account name and follows account scope and history", async () => {
let resolveAccounts!: () => void;
const accountsReady = new Promise<void>((resolve) => {
resolveAccounts = resolve;
});
server.use(
http.get("*/api/v1/brokerage-accounts", async () => {
await accountsReady;
return HttpResponse.json([{ id: "a", name: "ИИС" }]);
}),
);
renderTitle("/accounts/a/income?from=2026-01-01&to=2026-08-31");
expect(document.title).toBe("Пассивный доход — Счет — WealthLedger");
await act(async () => resolveAccounts());
await waitFor(() =>
expect(document.title).toBe("Пассивный доход — ИИС — WealthLedger"),
);
fireEvent.click(screen.getByRole("button", { name: "Все счета" }));
expect(document.title).toBe("Пассивный доход — Все счета — WealthLedger");
fireEvent.click(screen.getByRole("button", { name: "Назад" }));
expect(document.title).toBe("Пассивный доход — ИИС — WealthLedger");
});
it.each([
["bonds", "SU26238RMFS4", "Облигация", "ОФЗ 26238"],
["stocks", "SBER", "Акция", "Сбербанк"],
])(
"updates the %s title when instrument data arrives",
async (kind, id, fallback, name) => {
server.use(
http.get(`*/api/v1/${kind}/by-ticker/:ticker`, () =>
HttpResponse.json({ name }),
),
);
renderTitle(`/${kind}/${id}`);
expect(document.title).toBe(`${fallback} — WealthLedger`);
await waitFor(() =>
expect(document.title).toBe(`${name} — WealthLedger`),
);
},
);
it("keeps a useful title when the instrument is unavailable", async () => {
let requested = false;
server.use(
http.get("*/api/v1/stocks/by-ticker/:ticker", () => {
requested = true;
return HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 });
}),
);
renderTitle("/stocks/missing");
await waitFor(() => expect(requested).toBe(true));
expect(document.title).toBe("Акция — WealthLedger");
});
});
@@ -0,0 +1,100 @@
import { useDocumentTitle } from "@mantine/hooks";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { useMatch } from "react-router";
import { useBankCard } from "@/entities/bank-card";
import { useBond } from "@/entities/bond";
import { useBrokerageAccounts } from "@/entities/brokerage-account";
import { useStock } from "@/entities/stock";
type DocumentTitleProps = { title: string };
const DocumentTitle: FC<DocumentTitleProps> = (props) => {
const { t } = useTranslation();
useDocumentTitle(`${props.title} — ${t("appName")}`);
return null;
};
type AccountDocumentTitleProps = {
accountId: string;
section?: string;
};
const AccountDocumentTitle: FC<AccountDocumentTitleProps> = (props) => {
const { t } = useTranslation();
const { data } = useBrokerageAccounts();
const accountName =
props.accountId === "all"
? t("accounts.all")
: (data?.find((account) => account.id === props.accountId)?.name ??
t("account.title"));
const section =
props.section === "income"
? t("income.title")
: props.section === "operations"
? t("ledger.title")
: props.section === "events"
? t("events.title")
: t("portfolio.title");
return <DocumentTitle title={`${section} — ${accountName}`} />;
};
type InstrumentDocumentTitleProps = { identifier: string };
const BondDocumentTitle: FC<InstrumentDocumentTitleProps> = (props) => {
const { t } = useTranslation();
const { data } = useBond(props.identifier);
return <DocumentTitle title={data?.name ?? t("bond.instrument")} />;
};
const StockDocumentTitle: FC<InstrumentDocumentTitleProps> = (props) => {
const { t } = useTranslation();
const { data } = useStock(props.identifier);
return <DocumentTitle title={data?.name ?? t("stock.share")} />;
};
const CardDocumentTitle: FC<InstrumentDocumentTitleProps> = (props) => {
const { t } = useTranslation();
const { data } = useBankCard(props.identifier);
return <DocumentTitle title={data?.name ?? t("bank.card")} />;
};
const ProtectedDocumentTitle: FC = () => {
const { t } = useTranslation();
const account = useMatch("/accounts/:accountId/:section?");
const bond = useMatch("/bonds/:bondId");
const stock = useMatch("/stocks/:stockId");
const card = useMatch("/cards/:cardId");
const cards = useMatch("/cards");
const bonds = useMatch("/bonds");
const stocks = useMatch("/stocks");
const settings = useMatch("/settings");
if (card) return <CardDocumentTitle identifier={card.params.cardId ?? ""} />;
if (account)
return (
<AccountDocumentTitle
accountId={account.params.accountId ?? ""}
section={account.params.section}
/>
);
if (bond) return <BondDocumentTitle identifier={bond.params.bondId ?? ""} />;
if (stock)
return <StockDocumentTitle identifier={stock.params.stockId ?? ""} />;
return (
<DocumentTitle
title={t(
cards
? "navigation.cards"
: bonds
? "navigation.bonds"
: stocks
? "navigation.stocks"
: settings
? "navigation.settings"
: "navigation.accounts",
)}
/>
);
};
export { ProtectedDocumentTitle };