Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/treasuryOverviewPage/Stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface Stream {
id: string;
recipient: string;
rate: string;
accruedAmount?: string | number;
accruedAmount: number;
status: StreamStatus;
startDate?: string;
startDate: string;
}
8 changes: 3 additions & 5 deletions src/components/treasuryOverviewPage/StreamRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,9 @@ export default function StreamRow({

<td className="stream-row__cell py-4 px-3" data-label="RATE" style={{ color: "var(--color-text-primary)" }}>
<div className="stream-row__amount">{stream.rate}</div>
{typeof stream.accruedAmount === "number" && (
<div className="stream-row__amount text-xs" style={{ color: "var(--color-text-muted)" }}>
{formatAccruedAmount(stream.accruedAmount)}
</div>
)}
<div className="stream-row__amount text-xs" style={{ color: "var(--color-text-muted)" }}>
{formatAccruedAmount(stream.accruedAmount)}
</div>
</td>

<td className="stream-row__cell py-4 px-3" data-label="STATUS">
Expand Down
106 changes: 106 additions & 0 deletions src/components/treasuryOverviewPage/__tests__/StreamRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const stream: Stream = {
rate: "2,500 USDC/mo",
accruedAmount: 1234.56,
status: "Active",
startDate: "2026-01-01",
};

function renderRow(rowStream: Stream = stream) {
Expand Down Expand Up @@ -206,5 +207,110 @@ describe("StreamRow", () => {
// Menu should close after selection
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});

describe("Regression tests for issue #1459", () => {
it("renders accrued amount for streams with zero accrued amount", () => {
renderRow({
...stream,
id: "STR-ZERO",
accruedAmount: 0,
});

expect(screen.getByText("0 USDC accrued")).toBeInTheDocument();
});

it("renders status for completed streams", () => {
renderRow({
...stream,
id: "STR-COMP",
status: "Completed",
});

expect(screen.getByRole("status", { name: "Completed status" })).toHaveTextContent("COMPLETED");
});

it("renders status for paused streams", () => {
renderRow({
...stream,
id: "STR-PAUSED",
status: "Paused",
});

expect(screen.getByRole("status", { name: "Paused status" })).toHaveTextContent("PAUSED");
});

it("renders status for active streams", () => {
renderRow({
...stream,
id: "STR-ACTIVE",
status: "Active",
});

expect(screen.getByRole("status", { name: "Active status" })).toHaveTextContent("ACTIVE");
});

it("renders accrued amount for streams with large values", () => {
renderRow({
...stream,
id: "STR-LARGE",
accruedAmount: 999999999.99,
});

expect(screen.getByText("999,999,999.99 USDC accrued")).toBeInTheDocument();
});

it("renders all required fields without defensive checks", () => {
renderRow({
id: "STR-ALL-FIELDS",
name: "Test Stream",
recipient: "GTEST123456789ABCDEF",
rate: "1,000 USDC/mo",
accruedAmount: 5000,
status: "Active",
startDate: "2026-01-01",
});

expect(screen.getByText("Test Stream")).toBeInTheDocument();
expect(screen.getByText("GTEST1...CDEF")).toBeInTheDocument();
expect(screen.getByText("1,000 USDC/mo")).toBeInTheDocument();
expect(screen.getByText("5,000 USDC accrued")).toBeInTheDocument();
expect(screen.getByRole("status", { name: "Active status" })).toHaveTextContent("ACTIVE");
});

it("consistently renders status matching normalized streamRecords data", () => {
// Test all three valid statuses from streamRecords StreamStatus type
const statuses: Array<"Active" | "Paused" | "Completed"> = ["Active", "Paused", "Completed"];

statuses.forEach((status) => {
const { unmount } = render(
<MemoryRouter>
<table>
<tbody>
<StreamRow
stream={{ ...stream, id: `STR-${status}`, status }}
onSelect={vi.fn()}
/>
</tbody>
</table>
</MemoryRouter>
);

expect(screen.getByRole("status", { name: `${status} status` })).toHaveTextContent(status.toUpperCase());
unmount();
});
});

it("renders accrued amount as number consistently with streamRecords normalization", () => {
// streamRecords.readNumber() returns 0 for invalid values, so we test that
// the UI renders the numeric value without type checking
renderRow({
...stream,
id: "STR-NUMERIC",
accruedAmount: 0,
});

expect(screen.getByText(/0.*USDC accrued/)).toBeInTheDocument();
});
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ describe("StreamRow with records missing optional fields", () => {
},
);

it("omits the accrued line when the optional accruedAmount is absent", () => {
const stream = toLegacyStream(makeStreamRecord());
delete stream.accruedAmount;
it("renders the accrued line even when source data is missing (normalization provides fallback)", () => {
// With the new design, accruedAmount is always required and the normalization
// layer provides a fallback of 0 when source data is missing
const record = normalizeStreamRecord({ id: "STR-NO-AMOUNT" });
const stream = toLegacyStream(record);
renderRow(stream);

expect(screen.getByText(stream.name)).toBeInTheDocument();
expect(screen.queryByText(/accrued/)).not.toBeInTheDocument();
expect(screen.getByText(/accrued/)).toBeInTheDocument();
});

it("renders the accrued line when accruedAmount is present", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ describe("useTreasuryOverviewData", () => {
rate: "1,200 XLM/mo",
accruedAmount: 500,
status: "Active",
startDate: "",
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function toLegacyStream(record: StreamRecord): Stream {
rate: formatMonthlyRate(record),
accruedAmount: record.streamedAmount,
status: record.status,
startDate: record.startDate,
startDate: record.startDate || "",
};
}

Expand Down
Loading