refactor: enforce frontend view model boundaries
This commit is contained in:
+59
-29
@@ -24,8 +24,10 @@ import { server } from "@/test/server";
|
||||
|
||||
import { AccountOperationsPage } from "./AccountOperationsPage";
|
||||
|
||||
const operation: BrokerageOperation = (/* 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. */
|
||||
|
||||
const operation: BrokerageOperation
|
||||
= {
|
||||
accruedInterest: null,
|
||||
category: "trade",
|
||||
commission: { currency: "RUB", value: "1" },
|
||||
@@ -41,18 +43,21 @@ const operation: BrokerageOperation = (/* eslint-disable unicorn/no-null -- Fixt
|
||||
quantity: "2",
|
||||
ticker: "SBER",
|
||||
type: "OPERATION_TYPE_BUY",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */);
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
const Navigation: FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<output aria-label="URL">{location.search}</output>
|
||||
|
||||
<button onClick={() => navigate(-1)} type="button">
|
||||
History back
|
||||
</button>
|
||||
|
||||
<button onClick={() => navigate(1)} type="button">
|
||||
History forward
|
||||
</button>
|
||||
@@ -225,6 +230,7 @@ describe("AccountOperationsPage", () => {
|
||||
"*/api/v1/brokerage-accounts/:accountId/operations",
|
||||
({ request }) => {
|
||||
searches.push(new URL(request.url).searchParams);
|
||||
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
hasNext: true,
|
||||
@@ -249,6 +255,7 @@ describe("AccountOperationsPage", () => {
|
||||
initialIndex={0}
|
||||
>
|
||||
<Navigation />
|
||||
|
||||
<Routes>
|
||||
<Route
|
||||
element={<AccountOperationsPage />}
|
||||
@@ -262,6 +269,7 @@ describe("AccountOperationsPage", () => {
|
||||
await screen.findByLabelText("Период");
|
||||
expect(searches[0].get("from")).toBeNull();
|
||||
expect(searches[0].get("to")).toBeNull();
|
||||
|
||||
for (const expected of [
|
||||
"?categories=trade",
|
||||
"?categories=trade",
|
||||
@@ -276,6 +284,7 @@ describe("AccountOperationsPage", () => {
|
||||
);
|
||||
await screen.findByLabelText("Период");
|
||||
}
|
||||
|
||||
expect(searches.at(-1)?.get("from")).toBe("2026-02-01T00:00:00.000Z");
|
||||
expect(searches.at(-1)?.get("to")).toBeNull();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Далее" }));
|
||||
@@ -313,9 +322,11 @@ describe("AccountOperationsPage", () => {
|
||||
new URL(request.url).searchParams.has("from")
|
||||
? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 })
|
||||
: HttpResponse.json(
|
||||
resource((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{ hasNext: false, items: [], nextCursor: null }
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
resource(
|
||||
{ hasNext: false, items: [], nextCursor: null },
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -328,6 +339,7 @@ describe("AccountOperationsPage", () => {
|
||||
]}
|
||||
>
|
||||
<Navigation />
|
||||
|
||||
<Routes>
|
||||
<Route
|
||||
element={<AccountOperationsPage />}
|
||||
@@ -354,16 +366,19 @@ describe("AccountOperationsPage", () => {
|
||||
({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
searches.push(url.search);
|
||||
|
||||
return HttpResponse.json(
|
||||
resource((/* 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. */
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
hasNext: !url.searchParams.get("cursor"),
|
||||
items: [operation],
|
||||
nextCursor: url.searchParams.get("cursor") ? null : "next-1",
|
||||
observedAt: "2026-08-14T00:00:00Z",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -394,16 +409,19 @@ describe("AccountOperationsPage", () => {
|
||||
({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
searches.push(url.search);
|
||||
|
||||
return HttpResponse.json(
|
||||
resource((/* 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. */
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
hasNext: false,
|
||||
items: [operation],
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-14T00:00:00Z",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -431,16 +449,19 @@ describe("AccountOperationsPage", () => {
|
||||
({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
searches.push(url.search);
|
||||
|
||||
return HttpResponse.json(
|
||||
resource((/* 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. */
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
hasNext: false,
|
||||
items: [operation],
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-14T00:00:00Z",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -484,17 +505,26 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
|
||||
"*/api/v1/brokerage-accounts/:accountId/operations",
|
||||
({ request }) => {
|
||||
requested.push(Object.fromEntries(new URL(request.url).searchParams));
|
||||
|
||||
return HttpResponse.json(
|
||||
resource(null, (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
resource(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
null,
|
||||
{
|
||||
sync: { errorCode: "TBANK_UNAVAILABLE", id: null, state: "failed" },
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */)),
|
||||
sync: {
|
||||
errorCode: "TBANK_UNAVAILABLE",
|
||||
id: null,
|
||||
state: "failed",
|
||||
},
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
http.post("*/api/v1/tbank-sync", async ({ request }) => {
|
||||
syncRequests.push(await request.json());
|
||||
|
||||
return HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 });
|
||||
}),
|
||||
);
|
||||
@@ -529,16 +559,15 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
resource(
|
||||
(/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{
|
||||
accountId: "account-1",
|
||||
hasNext: false,
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */),
|
||||
{ availability: "partial" },
|
||||
{
|
||||
accountId: "account-1",
|
||||
hasNext: false,
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ { availability: "partial" },
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -562,6 +591,7 @@ it("recovers an invalid cursor at the first page without clearing applied filter
|
||||
{ code: "INVALID_OPERATIONS_CURSOR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
return HttpResponse.json(
|
||||
resource({
|
||||
hasNext: true,
|
||||
|
||||
Reference in New Issue
Block a user