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,26 +21,31 @@ const brokerageAccountsQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts",
).queryKey;
const portfolioQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/portfolio",
{ params: { path: { accountId: "account-1" } } },
).queryKey;
const operationsQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/operations",
{ params: { path: { accountId: "account-1" } } },
).queryKey;
const stockQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/stocks/{uid}/position",
{ params: { path: { uid: "e6123145-9665-43e0-8413-cd61b8aa9b13" } } },
).queryKey;
const bondQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/bonds/{uid}/position",
{ params: { path: { uid: "e6123145-9665-43e0-8413-cd61b8aa9b13" } } },
).queryKey;
const bankCardsQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/bank-cards",
@@ -48,6 +53,7 @@ const bankCardsQueryKey = apiQueryClient.queryOptions(
params: { query: { includeArchived: "false" } },
},
).queryKey;
const unrelatedQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/auth/me",
@@ -59,6 +65,7 @@ function renderSection(client: QueryClient) {
clear: vi.fn(),
refresh: () => Promise.resolve(true),
});
return render(
<MantineProvider>
<QueryClientProvider client={client}>
@@ -70,21 +77,22 @@ function renderSection(client: QueryClient) {
function seededClient() {
const client = createTestQueryClient();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
client.setQueryData(brokerageAccountsQueryKey, [
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
id: "account-1",
name: "Основной счет",
openedOn: null,
type: "BROKERAGE",
}
/* eslint-enable unicorn/no-null */),
{
id: "account-1",
name: "Основной счет",
openedOn: null,
type: "BROKERAGE",
},
/* eslint-enable unicorn/no-null */
]);
client.setQueryData(portfolioQueryKey, { source: "previous-connection" });
client.setQueryData(stockQueryKey, { source: "previous-connection" });
client.setQueryData(bondQueryKey, { source: "previous-connection" });
client.setQueryData(unrelatedQueryKey, { id: "user-1" });
client.setQueryData(bankCardsQueryKey, [{ id: "bank-card-1" }]);
return client;
}
@@ -92,18 +100,23 @@ describe("TbankConnectionSection", () => {
it("preserves cached status and a draft entered during a delayed failed refetch", async () => {
const status = { connected: true, connectedAt: "2026-08-14T00:00:00Z" };
const { promise: response, resolve: finishRefetch } = Promise.withResolvers<void>();
const { promise: response, resolve: finishRefetch }
= Promise.withResolvers<void>();
let reads = 0;
server.use(
http.get("*/api/v1/tbank-connection", async () => {
reads += 1;
if (reads === 1) {
await response;
return HttpResponse.json(
{ code: "TBANK_UNAVAILABLE" },
{ status: 503 },
);
}
return HttpResponse.json(status);
}),
);
@@ -141,11 +154,14 @@ describe("TbankConnectionSection", () => {
server.use(
http.get("*/api/v1/tbank-connection", () => {
reads += 1;
return reads === 1
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
: HttpResponse.json((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{ connected: false, connectedAt: null }
/* eslint-enable unicorn/no-null */));
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
: HttpResponse.json(
{ connected: false, connectedAt: null },
/* eslint-enable unicorn/no-null */
);
}),
);
renderSection(createTestQueryClient());
@@ -244,19 +260,24 @@ describe("TbankConnectionSection", () => {
const client = seededClient();
let isAborted = false;
const { promise: previousResponse, resolve: finishPrevious } = Promise.withResolvers<{ source: string }>();
const { promise: previousResponse, resolve: finishPrevious }
= Promise.withResolvers<{ source: string }>();
const previousRequest = client
.query({
queryFn: ({ signal }) => {
signal.addEventListener("abort", () => {
isAborted = true;
});
return previousResponse;
},
queryKey: operationsQueryKey,
})
// eslint-disable-next-line unicorn/prefer-await -- Attach the handler immediately and retain the pending promise for queue/race ordering.
.catch(() => {});
renderSection(client);
await screen.findByText("Не подключено");
@@ -302,7 +323,9 @@ describe("TbankConnectionSection", () => {
it("keeps DELETE recovery inside the dialog and preserves cache until successful retry", async () => {
let deletes = 0;
const { promise: response, resolve: finishDelete } = Promise.withResolvers<void>();
const { promise: response, resolve: finishDelete }
= Promise.withResolvers<void>();
server.use(
http.get("*/api/v1/tbank-connection", () =>
HttpResponse.json({
@@ -313,6 +336,7 @@ describe("TbankConnectionSection", () => {
http.delete("*/api/v1/tbank-connection", async () => {
deletes += 1;
if (deletes === 1) await response;
return deletes === 1
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
: new HttpResponse(null, { status: 204 });
@@ -324,9 +348,11 @@ describe("TbankConnectionSection", () => {
fireEvent.click(
await screen.findByRole("button", { name: "Удалить подключение" }),
);
const dialog = await screen.findByRole("dialog", {
name: "Удалить подключение?",
});
await waitFor(() =>
// eslint-disable-next-line testing-library/no-node-access -- This assertion checks DOM structure, decoration or focus behavior that semantic queries cannot establish.
expect(dialog.contains(document.activeElement)).toBe(true),