forked from SmartDropLabs/smartdrop-frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseLeaderboard.test.ts
More file actions
158 lines (127 loc) · 5.33 KB
/
Copy pathuseLeaderboard.test.ts
File metadata and controls
158 lines (127 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@/test/renderHook";
import type { SortKey, LeaderboardEntry } from "./useLeaderboard";
// Manually resolvable promises for controlling resolution order
function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
const mockEntries: LeaderboardEntry[] = [
{ address: "addr-credits", totalCredits: 100, totalStake: 500, boostUtilization: 0.5 },
{ address: "addr-other", totalCredits: 50, totalStake: 200, boostUtilization: 0.3 },
];
const mockStakeEntries: LeaderboardEntry[] = [
{ address: "addr-stake", totalCredits: 10, totalStake: 999, boostUtilization: 0.9 },
];
let fetchLeaderboardMock: ReturnType<typeof vi.fn>;
vi.mock("./useLeaderboard", async (importOriginal) => {
const actual = await importOriginal<typeof import("./useLeaderboard")>();
return {
...actual,
fetchLeaderboard: (...args: unknown[]) => fetchLeaderboardMock(...args),
};
});
// We need to import after mocking
const { useLeaderboard, fetchLeaderboard } = await import("./useLeaderboard");
describe("useLeaderboard race condition", () => {
beforeEach(() => {
vi.useFakeTimers();
fetchLeaderboardMock = vi.fn();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("reflects the most recently issued request, not whichever resolved last", async () => {
const deferredCredits = createDeferred<{ entries: LeaderboardEntry[]; total: number }>();
const deferredStake = createDeferred<{ entries: LeaderboardEntry[]; total: number }>();
// First call (credits sort) returns slow promise
// Second call (stake sort) returns fast promise
fetchLeaderboardMock
.mockReturnValueOnce(deferredCredits.promise)
.mockReturnValueOnce(deferredStake.promise);
const { result, rerender } = renderHook(() => useLeaderboard(null));
// Initial fetch fires via effect — resolve it so hook stabilizes
await act(async () => {
deferredCredits.resolve({ entries: mockEntries, total: 2 });
await deferredCredits.promise;
});
// Now trigger sort change → issues a new fetch with stake sort
await act(async () => {
result.current.setSortKey("stake");
rerender();
});
// Resolve the SECOND request (stake) before the first (credits)
await act(async () => {
deferredStake.resolve({ entries: mockStakeEntries, total: 1 });
await deferredStake.promise;
});
// The hook should show stake entries (the most recently issued request)
expect(result.current.paged).toEqual(mockStakeEntries);
});
it("discards stale responses from earlier requests", async () => {
const deferredFirst = createDeferred<{ entries: LeaderboardEntry[]; total: number }>();
const deferredSecond = createDeferred<{ entries: LeaderboardEntry[]; total: number }>();
fetchLeaderboardMock
.mockReturnValueOnce(deferredFirst.promise)
.mockReturnValueOnce(deferredSecond.promise);
const { result, rerender } = renderHook(() => useLeaderboard(null));
// Wait for initial fetch to be in-flight
await act(async () => {
// Let the initial fetch start but don't resolve it yet
});
// Trigger sort change → second request
await act(async () => {
result.current.setSortKey("stake");
rerender();
});
// Resolve second (newer) request first
await act(async () => {
deferredSecond.resolve({ entries: mockStakeEntries, total: 1 });
await deferredSecond.promise;
});
// Then resolve first (stale) request
await act(async () => {
deferredFirst.resolve({ entries: mockEntries, total: 2 });
await deferredFirst.promise;
});
// Should still show the newer (stake) data, not the stale (credits) data
expect(result.current.paged).toEqual(mockStakeEntries);
});
it("auto-refresh does not overwrite newer manual refresh data", async () => {
const deferredAuto = createDeferred<{ entries: LeaderboardEntry[]; total: number }>();
const deferredManual = createDeferred<{ entries: LeaderboardEntry[]; total: number }>();
fetchLeaderboardMock
.mockReturnValueOnce(Promise.resolve({ entries: mockEntries, total: 2 })) // initial
.mockReturnValueOnce(deferredAuto.promise) // auto-refresh
.mockReturnValueOnce(deferredManual.promise); // manual refresh
const { result, rerender } = renderHook(() => useLeaderboard(null));
// Let initial fetch resolve
await act(async () => {
vi.advanceTimersByTime(0);
});
// Advance past initial render, then trigger a manual refresh
// while auto-refresh is in flight
await act(async () => {
result.current.setSortKey("stake");
rerender();
});
// Resolve auto-refresh (stale) before manual (fresh)
await act(async () => {
deferredAuto.resolve({ entries: mockEntries, total: 2 });
await deferredAuto.promise;
});
// Resolve manual refresh (fresh)
await act(async () => {
deferredManual.resolve({ entries: mockStakeEntries, total: 1 });
await deferredManual.promise;
});
// Should show manual refresh data
expect(result.current.paged).toEqual(mockStakeEntries);
});
});