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
121 changes: 104 additions & 17 deletions frontend/src/hooks/__tests__/useOwnerWeights.test.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,122 @@
import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import * as contract from "../../lib/contract";
import { useOwnerWeights } from "../useOwnerWeights";

vi.mock("../../lib/contract", () => ({
getOwnerWeights: vi.fn(),
}));

const intervalMs = 5000;
const initialWeights = [
{ address: "GOWNER111", weight: 4 },
{ address: "GOWNER222", weight: 6 },
];
const refreshedWeights = [
{ address: "GOWNER111", weight: 3 },
{ address: "GOWNER222", weight: 7 },
];

describe("useOwnerWeights", () => {
test("returns no weights for an empty owner list", () => {
const { result } = renderHook(() => useOwnerWeights([]));
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
vi.spyOn(console, "error").mockImplementation(() => undefined);
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

test("fetches owner weights on mount", async () => {
vi.mocked(contract.getOwnerWeights).mockResolvedValueOnce(initialWeights);

const { result } = renderHook(() => useOwnerWeights(intervalMs));

expect(result.current).toEqual({
weightsByAddress: {},
totalWeight: 0,
loading: false,
ownerWeights: [],
loading: true,
error: null,
});

await vi.waitFor(() => {
expect(result.current.loading).toBe(false);
});

expect(contract.getOwnerWeights).toHaveBeenCalledTimes(1);
expect(result.current.ownerWeights).toEqual(initialWeights);
expect(result.current.error).toBeNull();
});

test("loads current flat voting weights for owners", async () => {
const ownerAddresses = ["GOWNER111", "GOWNER222"];
test("refreshes owner weights on an interval", async () => {
vi.mocked(contract.getOwnerWeights)
.mockResolvedValueOnce(initialWeights)
.mockResolvedValueOnce(refreshedWeights);

const { result } = renderHook(() => useOwnerWeights(intervalMs));

const { result } = renderHook(() => useOwnerWeights(ownerAddresses));
await vi.waitFor(() => {
expect(result.current.ownerWeights).toEqual(initialWeights);
});

expect(result.current.loading).toBe(true);
act(() => {
vi.advanceTimersByTime(intervalMs);
});

await waitFor(() => {
expect(result.current.loading).toBe(false);
await vi.waitFor(() => {
expect(contract.getOwnerWeights).toHaveBeenCalledTimes(2);
expect(result.current.ownerWeights).toEqual(refreshedWeights);
});

expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.totalWeight).toBe(2);
expect(result.current.weightsByAddress).toEqual({
GOWNER111: 1,
GOWNER222: 1,
});

test("keeps cached owner weights when a refresh fails", async () => {
const refreshError = new Error("RPC unavailable");
vi.mocked(contract.getOwnerWeights)
.mockResolvedValueOnce(initialWeights)
.mockRejectedValueOnce(refreshError);

const { result } = renderHook(() => useOwnerWeights(intervalMs));

await vi.waitFor(() => {
expect(result.current.ownerWeights).toEqual(initialWeights);
});

act(() => {
vi.advanceTimersByTime(intervalMs);
});

await vi.waitFor(() => {
expect(contract.getOwnerWeights).toHaveBeenCalledTimes(2);
expect(result.current.error).toBe("RPC unavailable");
});

expect(result.current.ownerWeights).toEqual(initialWeights);
expect(result.current.loading).toBe(false);
expect(console.error).toHaveBeenCalledWith(
"Failed to fetch owner weights",
refreshError,
);
});

test("stops refreshing after unmount", async () => {
vi.mocked(contract.getOwnerWeights).mockResolvedValueOnce(initialWeights);

const { result, unmount } = renderHook(() => useOwnerWeights(intervalMs));

await vi.waitFor(() => {
expect(result.current.ownerWeights).toEqual(initialWeights);
});

unmount();

act(() => {
vi.advanceTimersByTime(intervalMs);
});

expect(contract.getOwnerWeights).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions frontend/src/hooks/useOwnerWeights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function useOwnerWeights(ownerAddresses: string[]) {

return () => {
cancelled = true;
clearInterval(intervalId);
};
}, [ownerAddresses]);

Expand Down
17 changes: 7 additions & 10 deletions frontend/src/pages/OwnersPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,17 @@ describe("OwnersPage", () => {

test("shows weighted quorum and each owner voting share", () => {
mockUseOwnerWeights.mockReturnValue({
weightsByAddress: {
GOWNER111: 5,
GOWNER222: 15,
},
totalWeight: 20,
ownerWeights: [
{ address: "GOWNER111", weight: 5 },
{ address: "GOWNER222", weight: 15 },
],
loading: false,
error: null,
});

renderOwnersPage();

expect(mockUseOwnerWeights).toHaveBeenCalledWith(ownerAddresses);
expect(mockUseOwnerWeights).toHaveBeenCalledWith();
expect(screen.getByText("Requires 5 of 20 voting weight")).toBeInTheDocument();
expect(screen.getByText("25.0% of voting power must approve."))
.toBeInTheDocument();
Expand All @@ -60,8 +59,7 @@ describe("OwnersPage", () => {

test("keeps owners visible while voting weights load", () => {
mockUseOwnerWeights.mockReturnValue({
weightsByAddress: {},
totalWeight: 0,
ownerWeights: [],
loading: true,
error: null,
});
Expand All @@ -79,8 +77,7 @@ describe("OwnersPage", () => {

test("keeps owners visible when voting weights fail to load", () => {
mockUseOwnerWeights.mockReturnValue({
weightsByAddress: {},
totalWeight: 0,
ownerWeights: [],
loading: false,
error: "Failed to load owner weights",
});
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/OwnersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,14 @@ export function OwnersPage({
<p>
{ownerWeightsLoading
? `Loading voting power across ${ownerCountLabel}...`
: ownerWeightsError
: weightsUnavailable
? "Voting power unavailable; owners remain visible."
: `${quorumPercent} of voting power must approve.`}
</p>
{ownerWeightsError && (
{weightsStale && (
<p className="text-amber-400">Voting weights may be stale.</p>
)}
{weightsUnavailable && (
<p className="text-amber-400">Voting weights unavailable.</p>
)}
</div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types/accord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export type Owner = {
label: string;
};

export type OwnerWeight = {
address: string;
weight: number;
};

export type DashboardStat = {
label: string;
value: string;
Expand Down
Loading