feat: render hourly investment states
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 7m41s

This commit was merged in pull request #51.
This commit is contained in:
2026-10-09 21:13:34 +03:00
committed by ksv741
parent 20b2cdfd43
commit 3c226f8c29
36 changed files with 1388 additions and 1238 deletions
@@ -5,20 +5,23 @@ import { resource } from "@/test/resource";
import { section } from "./section";
describe("instrument source and transport presentation", () => {
it.each(["missing", "partial", "complete"] as const)(
it.each([
["initial-loading", "incomplete"],
["available", "complete"],
] as const)(
"keeps %s separate from successful transport",
(availability) => {
const data = availability === "missing" ? undefined : { rows: [] };
(availability, completeness) => {
const data = availability === "initial-loading" ? undefined : { rows: [] };
const state = section({
data,
isError: false,
meta: resource(data, { availability }).meta,
meta: resource(data, { availability, completeness }).meta,
});
expect(state.loading).toBe(availability === "missing");
expect(state.partial).toBe(availability === "partial");
expect(state.hasData).toBe(availability !== "missing");
expect(state.loading).toBe(availability === "initial-loading");
expect(state.partial).toBe(completeness === "incomplete");
expect(state.hasData).toBe(availability !== "initial-loading");
},
);
it("preserves empty successful data instead of treating it as missing", () => {
@@ -37,7 +40,12 @@ describe("instrument source and transport presentation", () => {
it("preserves a full source when force fails", () => {
const meta = resource([], {
sync: { errorCode: "TBANK_UNAVAILABLE", id: "work", state: "failed" },
lastAttempt: {
finishedAt: "2026-08-14T00:01:00Z",
problems: [],
startedAt: "2026-08-14T00:00:00Z",
state: "failed",
},
}).meta;
const state = section({ data: [], isError: false, meta });
@@ -53,7 +61,12 @@ describe("instrument source and transport presentation", () => {
data: undefined,
isError: false,
meta: resource(null, {
sync: { errorCode: "TBANK_UNAVAILABLE", id: "work", state: "failed" },
lastAttempt: {
finishedAt: "2026-08-14T00:01:00Z",
problems: [],
startedAt: "2026-08-14T00:00:00Z",
state: "failed",
},
}).meta,
});
@@ -16,14 +16,14 @@ function section(query: {
isError: boolean;
meta?: InvestmentResourceMeta;
}): SectionState {
const isError = query.isError || query.meta?.sync.state === "failed";
const isError = query.isError || query.meta?.lastAttempt.state === "failed";
const hasData = hasSectionData(query.data);
return {
error: isError,
hasData,
loading: !hasData && !isError,
partial: query.meta?.availability === "partial",
partial: query.meta?.completeness === "incomplete",
};
}
@@ -19,12 +19,14 @@ function auth() {
}
describe("read-only screen synchronization", () => {
it("retries the resource with GET and never starts a sync POST", async () => {
it("retries the resource with GET without requesting source observation", async () => {
auth();
let gets = 0;
const syncModes: (null | string)[] = [];
server.use(
http.get("*/api/v1/brokerage-accounts", () => {
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
gets += 1;
syncModes.push(new URL(request.url).searchParams.get("sync"));
return gets === 1
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
@@ -49,6 +51,7 @@ describe("read-only screen synchronization", () => {
await waitFor(() => expect(result.current.accounts.data).toEqual([]));
expect(gets).toBe(2);
expect(syncModes).toEqual([null, null]);
});
it("refreshes the currently open resources together", async () => {
@@ -8,7 +8,6 @@ type ScreenResource = {
isError: boolean;
isFetching: boolean;
meta?: InvestmentResourceMeta;
observe: () => Promise<unknown>;
queryKey: QueryKey;
refetch: () => Promise<unknown>;
};
@@ -42,14 +41,12 @@ function useScreenSync(_scope: ScreenScope, resources: ScreenResource[]) {
await Promise.allSettled(resources.map(resource => resource.refetch()));
};
// Retry failed HTTP reads without asking the source to import again.
// Retry only the HTTP read; source attempts are reported in resource metadata.
const retry = async () => {
const failed = resources.filter(
resource => resource.isError,
);
const failed = resources.filter(resource => resource.isError);
if (failed.length > 0)
await Promise.allSettled(failed.map(resource => resource.observe()));
await Promise.allSettled(failed.map(resource => resource.refetch()));
else await refresh();
};
@@ -57,7 +54,9 @@ function useScreenSync(_scope: ScreenScope, resources: ScreenResource[]) {
refresh,
refreshing: isRefreshing,
retry,
sourceError: resources.some(({ meta }) => meta?.sync.state === "failed"),
sourceError: resources.some(
({ meta }) => meta?.lastAttempt.state === "failed",
),
transportError: resources.some(resource => resource.isError),
};
}