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
94 changes: 94 additions & 0 deletions apps/s03-indexer/tests/mappingHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,43 @@ describe("SO4 event dispatch", () => {
expect(withdrawal.cancellationReason).toBe("expired");
});

test("indexes withdrawal create event with all key fields", async () => {
await dispatchEvent(so4Event("wth_crt", lifecyclePayload("wth-2")));

const [withdrawal] = records("Withdrawal");
expect(records("Withdrawal")).toHaveLength(1);
expect(withdrawal.id).toBe("withdrawal:wth-2");
expect(withdrawal.status).toBe("CREATED");
expect(withdrawal.account).toBe(account);
expect(withdrawal.market).toBe(marketToken);
expect(withdrawal.createdLedger).toBe(100);
});

test("indexes withdrawal execute event", async () => {
await dispatchEvent(so4Event("wth_crt", lifecyclePayload("wth-3")));
await dispatchEvent(so4Event("wth_exe", lifecyclePayload("wth-3")));

const [withdrawal] = records("Withdrawal");
expect(records("Withdrawal")).toHaveLength(1);
expect(withdrawal.id).toBe("withdrawal:wth-3");
expect(withdrawal.status).toBe("EXECUTED");
expect(withdrawal.executedLedger).toBe(100);
});

test("withdrawal lifecycle events are idempotent on rerun", async () => {
const event1 = so4Event("wth_crt", lifecyclePayload("wth-4"));
const event2 = so4Event("wth_exe", lifecyclePayload("wth-4"));

await dispatchEvent(event1);
await dispatchEvent(event2);
await dispatchEvent(event1);
await dispatchEvent(event2);

expect(records("Withdrawal")).toHaveLength(1);
const [withdrawal] = records("Withdrawal");
expect(withdrawal.status).toBe("EXECUTED");
});

test("indexes order lifecycle updates", async () => {
await dispatchEvent(
so4Event("ord_crt", lifecyclePayload("ord-1", { order_type: "MARKET", is_long: true })),
Expand All @@ -384,6 +421,63 @@ describe("SO4 event dispatch", () => {
expect(order.acceptablePrice).toBe("2000");
});

test("indexes order freeze event", async () => {
await dispatchEvent(
so4Event("ord_crt", lifecyclePayload("ord-2", { order_type: "LIMIT", is_long: false })),
);
await dispatchEvent(
so4Event("ord_frz", lifecyclePayload("ord-2")),
);

const [order] = records("Order");
expect(records("Order")).toHaveLength(1);
expect(order.id).toBe("order:ord-2");
expect(order.status).toBe("FROZEN");
expect(order.isLong).toBe(false);
});

test("indexes order execute event", async () => {
await dispatchEvent(
so4Event("ord_crt", lifecyclePayload("ord-3", { order_type: "MARKET", is_long: true })),
);
await dispatchEvent(
so4Event("ord_exe", lifecyclePayload("ord-3")),
);

const [order] = records("Order");
expect(records("Order")).toHaveLength(1);
expect(order.id).toBe("order:ord-3");
expect(order.status).toBe("EXECUTED");
});

test("indexes order cancel event", async () => {
await dispatchEvent(
so4Event("ord_crt", lifecyclePayload("ord-4", { order_type: "LIMIT" })),
);
await dispatchEvent(
so4Event("ord_can", lifecyclePayload("ord-4", { reason: "user_request" })),
);

const [order] = records("Order");
expect(records("Order")).toHaveLength(1);
expect(order.id).toBe("order:ord-4");
expect(order.status).toBe("CANCELLED");
});

test("order lifecycle events are idempotent on rerun", async () => {
const event1 = so4Event("ord_crt", lifecyclePayload("ord-5", { order_type: "MARKET" }));
const event2 = so4Event("ord_exe", lifecyclePayload("ord-5"));

await dispatchEvent(event1);
await dispatchEvent(event2);
await dispatchEvent(event1);
await dispatchEvent(event2);

expect(records("Order")).toHaveLength(1);
const [order] = records("Order");
expect(order.status).toBe("EXECUTED");
});

test("indexes position changes and current position state", async () => {
await dispatchEvent(
so4Event("pos_inc", {
Expand Down
171 changes: 171 additions & 0 deletions apps/web/src/features/trade/components/CircuitBreakerBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { describe, expect, it, vi, beforeEach } from "vitest"
import { render, screen, fireEvent } from "@testing-library/react"
import { CircuitBreakerBanner } from "./CircuitBreakerBanner"
import * as circuitBreakerHook from "../hooks/useCircuitBreaker"

vi.mock("../hooks/useCircuitBreaker")

describe("CircuitBreakerBanner", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("renders nothing when status is not active", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: { active: false, message: "" },
isLoading: false,
error: null,
} as never)

const { container } = render(<CircuitBreakerBanner symbol="ETH" />)
expect(container.firstChild).toBeNull()
})

it("renders nothing when status is undefined", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: undefined,
isLoading: false,
error: null,
} as never)

const { container } = render(<CircuitBreakerBanner symbol="ETH" />)
expect(container.firstChild).toBeNull()
})

it("renders banner when status is active", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: {
active: true,
message: "Market frozen due to circuit breaker",
},
isLoading: false,
error: null,
} as never)

render(<CircuitBreakerBanner symbol="ETH" />)
expect(screen.getByText("Market frozen due to circuit breaker")).toBeInTheDocument()
})

it("has alert role for accessibility", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: {
active: true,
message: "Execution frozen",
},
isLoading: false,
error: null,
} as never)

render(<CircuitBreakerBanner symbol="BTC" />)
expect(screen.getByRole("alert")).toBeInTheDocument()
})

it("displays amber styling for warning severity", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: {
active: true,
message: "Caution: market freeze active",
},
isLoading: false,
error: null,
} as never)

const { container } = render(<CircuitBreakerBanner symbol="SOL" />)
const banner = container.querySelector("[role='alert']")
expect(banner?.className).toContain("bg-amber-500/10")
expect(banner?.className).toContain("border-amber-500/40")
expect(banner?.className).toContain("text-amber-200")
})

it("shows dismiss button with correct styling", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: {
active: true,
message: "Circuit breaker engaged",
},
isLoading: false,
error: null,
} as never)

const { container } = render(<CircuitBreakerBanner symbol="USDC" />)
const dismissBtn = container.querySelector("button")
expect(dismissBtn).toHaveTextContent("Dismiss")
expect(dismissBtn?.className).toContain("text-amber-300")
})

it("dismisses banner when dismiss button clicked", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: {
active: true,
message: "Execution frozen",
},
isLoading: false,
error: null,
} as never)

const { container, rerender } = render(<CircuitBreakerBanner symbol="ETH" />)
expect(screen.getByText("Execution frozen")).toBeInTheDocument()

const dismissBtn = screen.getByRole("button", { name: /Dismiss/ })
fireEvent.click(dismissBtn)

expect(container.querySelector("[role='alert']")).not.toBeInTheDocument()
})

it("hides banner after dismissal even if status remains active", () => {
const useCircuitBreakerMock = vi.mocked(circuitBreakerHook.useCircuitBreaker)
useCircuitBreakerMock.mockReturnValueOnce({
data: {
active: true,
message: "Market frozen",
},
isLoading: false,
error: null,
} as never)

const { container } = render(<CircuitBreakerBanner symbol="ETH" />)
expect(screen.getByText("Market frozen")).toBeInTheDocument()

const dismissBtn = screen.getByRole("button", { name: /Dismiss/ })
fireEvent.click(dismissBtn)

expect(container.querySelector("[role='alert']")).not.toBeInTheDocument()
})

it("uses symbol from props in hook", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: { active: false, message: "" },
isLoading: false,
error: null,
} as never)

render(<CircuitBreakerBanner symbol="TSLA" />)
expect(circuitBreakerHook.useCircuitBreaker).toHaveBeenCalledWith("TSLA")
})

it("handles undefined symbol", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: { active: false, message: "" },
isLoading: false,
error: null,
} as never)

render(<CircuitBreakerBanner symbol={undefined} />)
expect(circuitBreakerHook.useCircuitBreaker).toHaveBeenCalledWith(undefined)
})

it("displays explanatory copy with accessible text", () => {
vi.mocked(circuitBreakerHook.useCircuitBreaker).mockReturnValueOnce({
data: {
active: true,
message: "Trading is frozen: volatility limit exceeded",
},
isLoading: false,
error: null,
} as never)

render(<CircuitBreakerBanner symbol="VIX" />)
const banner = screen.getByRole("alert")
expect(banner).toHaveTextContent("Trading is frozen: volatility limit exceeded")
})
})
Loading
Loading