refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
@@ -21,6 +21,7 @@ const snapshot: Portfolio = {
structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "123.000000001" }],
};
function auth() {
configureAuthClient({
accessToken: () => "synthetic",
@@ -41,6 +42,7 @@ describe("persistent resource query lifecycle", () => {
const mode = new URL(request.url).searchParams.get("sync");
modes.push(mode);
if (mode) observations += 1;
return HttpResponse.json(
resource(snapshot, {
stateVersion: observations > 1 ? "new" : "old",
@@ -76,22 +78,26 @@ describe("persistent resource query lifecycle", () => {
),
),
);
const { result } = renderHook(() => usePortfolio("a"), {
wrapper: createQueryWrapper().wrapper,
});
await waitFor(() => expect(result.current.meta?.sync.state).toBe("failed"));
expect(result.current.data).toBeUndefined();
expect(result.current.isSuccess).toBe(false);
expect(result.current.status).toBe("pending");
expect(result.current.isPending).toBe(true);
expect(result.current.isLoading).toBe(false);
expect(result.current.transport).toEqual((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(result.current.transport).toEqual(
{
error: null,
fetchStatus: "idle",
status: "success",
}
/* eslint-enable unicorn/no-null */));
},
/* eslint-enable unicorn/no-null */
);
expect(result.current.meta?.availability).toBe("missing");
});
it("keeps the refetch envelope while exposing the refreshed resource payload", async () => {
@@ -103,9 +109,11 @@ describe("persistent resource query lifecycle", () => {
HttpResponse.json(isReady ? refreshed : resource(null)),
),
);
const { result } = renderHook(() => usePortfolio("a"), {
wrapper: createQueryWrapper().wrapper,
});
await waitFor(() =>
expect(result.current.transport.status).toBe("success"),
);
@@ -130,20 +138,25 @@ describe("persistent resource query lifecycle", () => {
it("rejects a late ordinary response when an observe response for that resource arrived first", async () => {
auth();
const { promise: blocked, resolve: release } = Promise.withResolvers<void>();
const { promise: blocked, resolve: release }
= Promise.withResolvers<void>();
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/portfolio",
async ({ request }) => {
if (!new URL(request.url).searchParams.has("sync")) await blocked;
return HttpResponse.json(resource(snapshot));
},
),
);
const old = apiClient.GET(
"/api/v1/brokerage-accounts/{accountId}/portfolio",
{ params: { path: { accountId: "a" } } },
);
// eslint-disable-next-line vitest/valid-expect -- Attach rejection handling before releasing the request; the assertion is awaited below.
const rejected = expect(old).rejects.toMatchObject({ name: "AbortError" });
await apiClient.GET("/api/v1/brokerage-accounts/{accountId}/portfolio", {
@@ -156,21 +169,28 @@ describe("persistent resource query lifecycle", () => {
auth();
let isDenied = false;
const { promise: blocked, resolve: release } = Promise.withResolvers<void>();
const { promise: blocked, resolve: release }
= Promise.withResolvers<void>();
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/portfolio",
async ({ params }) => {
if (params.accountId === "b") await blocked;
return isDenied && params.accountId === "a" ? HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 }) : HttpResponse.json(resource(snapshot));
return isDenied && params.accountId === "a"
? HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 })
: HttpResponse.json(resource(snapshot));
},
),
);
const { wrapper } = createQueryWrapper();
const { result } = renderHook(
() => ({ a: usePortfolio("a"), b: usePortfolio("b") }),
{ wrapper },
);
await waitFor(() => expect(result.current.a.data).toEqual(snapshot));
isDenied = true;
await act(() => result.current.a.refetch());
@@ -16,6 +16,7 @@ describe("usePortfolio", () => {
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
const { result } = renderHook(() => usePortfolio("account-1"), {
wrapper: createQueryWrapper().wrapper,
});
@@ -34,6 +35,7 @@ describe("usePortfolio", () => {
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
calls += 1;
return calls === 1
? HttpResponse.json(
resource({
@@ -48,6 +50,7 @@ describe("usePortfolio", () => {
: HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 });
}),
);
const { result } = renderHook(() => usePortfolio("account-1"), {
wrapper: createQueryWrapper().wrapper,
});
+2 -2
View File
@@ -8,7 +8,7 @@ type Portfolio = NonNullable<
paths["/api/v1/brokerage-accounts/{accountId}/portfolio"]["get"]["responses"][200]["content"]["application/json"]["data"]
>;
function portfolioQueryOptions(accountId: string | undefined) {
function portfolioQueryOptions(accountId?: string) {
return apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/portfolio",
@@ -16,7 +16,7 @@ function portfolioQueryOptions(accountId: string | undefined) {
);
}
function usePortfolio(accountId: string | undefined) {
function usePortfolio(accountId?: string) {
return useResourceQuery({
meta: { accessGroup: "brokerage" },
...portfolioQueryOptions(accountId),
+11 -3
View File
@@ -8,14 +8,22 @@ function formatShare(
numerator: Money | null,
denominator: Money | null,
): null | string {
if (!numerator || !denominator) return /* eslint-disable-line unicorn/no-null -- The existing formatter contract uses null for unavailable data. */ null;
if (!numerator || !denominator)
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null;
const total = Number(denominator.value);
if (!Number.isFinite(total) || total <= 0) return /* eslint-disable-line unicorn/no-null -- The existing formatter contract uses null for unavailable data. */ null;
if (!Number.isFinite(total) || total <= 0)
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null;
const percent = (Number(numerator.value) / total) * PERCENT_SCALE;
if (!Number.isFinite(percent)) return /* eslint-disable-line unicorn/no-null -- The existing formatter contract uses null for unavailable data. */ null;
if (!Number.isFinite(percent))
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null;
const rounded = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 1,
}).format(percent);
return `${rounded} %`;
}