forked from Liquifact/Liquifact-frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWalletProvider.consolidation.test.tsx
More file actions
277 lines (228 loc) · 9.43 KB
/
Copy pathWalletProvider.consolidation.test.tsx
File metadata and controls
277 lines (228 loc) · 9.43 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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
* Consolidation tests — assert a single canonical hook shape and that all
* consumers resolve it from the one source of truth: WalletProvider.jsx.
*
* Requirements validated here:
* 1. Only one useWallet() hook shape exists: { state, walletData, connect, disconnect }
* 2. WalletContext.jsx is a shim — it re-exports from WalletProvider.jsx
* 3. WALLET_STATES is identical whether imported from WalletProvider or WalletContext
* 4. toast side-effects fire on connect outcomes
* 5. localStorage rehydration still works
*/
import "@testing-library/jest-dom";
import { act, render, screen, waitFor } from "@testing-library/react";
import { ToastProvider } from "./ToastProvider";
import * as freighter from "../lib/wallet/freighter";
jest.mock("../lib/wallet/freighter");
// ── Canonical source ──────────────────────────────────────────────────────────
import {
WalletProvider as WalletProviderDirect,
useWallet as useWalletDirect,
WALLET_STATES as WALLET_STATES_DIRECT,
} from "./WalletProvider";
// ── Shim (WalletContext.jsx) ──────────────────────────────────────────────────
import {
WalletProvider as WalletProviderShim,
useWallet as useWalletShim,
WALLET_STATES as WALLET_STATES_SHIM,
} from "./WalletContext";
const STORAGE_KEY = "liquifact-wallet-snapshot";
// ── Probe component that reads the canonical hook shape ──────────────────────
function WalletProbe() {
const wallet = useWalletDirect();
// Assert canonical keys are present (TypeScript would catch this at compile-time;
// this assertion guards against accidental key renames at runtime)
const hasCanonicalShape =
"state" in wallet &&
"walletData" in wallet &&
typeof wallet.connect === "function" &&
typeof wallet.disconnect === "function";
return (
<div>
<span data-testid="has-canonical-shape">{String(hasCanonicalShape)}</span>
<span data-testid="wallet-state">{wallet.state}</span>
<button type="button" onClick={() => wallet.connect()}>
Connect
</button>
<button type="button" onClick={() => wallet.disconnect()}>
Disconnect
</button>
</div>
);
}
function renderWithProviders(ui = <WalletProbe />) {
return render(
<ToastProvider>
<WalletProviderDirect>{ui}</WalletProviderDirect>
</ToastProvider>
);
}
beforeEach(() => {
jest.useFakeTimers();
localStorage.clear();
jest.clearAllMocks();
process.env.NEXT_PUBLIC_STELLAR_NETWORK = "testnet";
});
afterEach(async () => {
await act(async () => {
jest.runOnlyPendingTimers();
});
jest.useRealTimers();
jest.restoreAllMocks();
localStorage.clear();
});
// ── 1. Single hook shape ──────────────────────────────────────────────────────
describe("Single hook shape", () => {
it("useWallet returns the canonical { state, walletData, connect, disconnect } shape", () => {
renderWithProviders();
expect(screen.getByTestId("has-canonical-shape")).toHaveTextContent("true");
});
it("starts in DISCONNECTED state", () => {
renderWithProviders();
expect(screen.getByTestId("wallet-state")).toHaveTextContent("disconnected");
});
it("throws when used outside WalletProvider", () => {
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
expect(() => render(<WalletProbe />)).toThrow("useWallet must be used within a WalletProvider");
spy.mockRestore();
});
});
// ── 2. Shim parity: WalletContext re-exports are identical references ─────────
describe("WalletContext shim exports canonical symbols", () => {
it("WalletProvider from shim === WalletProvider from source", () => {
expect(WalletProviderShim).toBe(WalletProviderDirect);
});
it("useWallet from shim === useWallet from source", () => {
expect(useWalletShim).toBe(useWalletDirect);
});
it("WALLET_STATES from shim deep-equals WALLET_STATES from source", () => {
expect(WALLET_STATES_SHIM).toEqual(WALLET_STATES_DIRECT);
});
it("WALLET_STATES from shim is the same object reference", () => {
expect(WALLET_STATES_SHIM).toBe(WALLET_STATES_DIRECT);
});
});
// ── 3. WALLET_STATES completeness ────────────────────────────────────────────
describe("WALLET_STATES contains all required states", () => {
const required = [
"DISCONNECTED",
"CONNECTING",
"CONNECTED",
"ERROR",
"WRONG_NETWORK",
"NO_WALLET",
];
required.forEach((key) => {
it(`includes ${key}`, () => {
expect(WALLET_STATES_DIRECT).toHaveProperty(key);
});
});
});
// ── 4. Toast side-effects fire on connection outcomes ─────────────────────────
describe("Toast side-effects", () => {
it("fires a success toast on successful connect", async () => {
(freighter.isFreighterConnected as jest.Mock).mockResolvedValue(true);
(freighter.connectFreighter as jest.Mock).mockResolvedValue("GABC...XYZ123");
(freighter.assertExpectedNetwork as jest.Mock).mockResolvedValue(undefined);
(freighter.getFreighterNetwork as jest.Mock).mockResolvedValue("public");
renderWithProviders();
await act(async () => {
screen.getByRole("button", { name: "Connect" }).click();
});
await waitFor(() => {
expect(screen.getByTestId("wallet-state")).toHaveTextContent("connected");
});
// Toast region has role="status" — find text from ToastProvider
expect(screen.getByText("Wallet connected")).toBeInTheDocument();
expect(screen.getByText("Wallet connected successfully.")).toBeInTheDocument();
});
it("fires an error toast on connection failure", async () => {
(freighter.isFreighterConnected as jest.Mock).mockResolvedValue(true);
(freighter.connectFreighter as jest.Mock).mockRejectedValue(
new Error("User rejected connection")
);
renderWithProviders();
await act(async () => {
screen.getByRole("button", { name: "Connect" }).click();
});
await waitFor(() => {
expect(screen.getByTestId("wallet-state")).toHaveTextContent("error");
});
expect(screen.getByText("Connection failed")).toBeInTheDocument();
});
it("fires an error toast on wrong network", async () => {
(freighter.isFreighterConnected as jest.Mock).mockResolvedValue(true);
(freighter.connectFreighter as jest.Mock).mockResolvedValue("GABC...XYZ123");
(freighter.assertExpectedNetwork as jest.Mock).mockRejectedValue(
new Error('Wallet is on "public" but the app requires "testnet"')
);
renderWithProviders();
await act(async () => {
screen.getByRole("button", { name: "Connect" }).click();
});
await waitFor(() => {
expect(screen.getByTestId("wallet-state")).toHaveTextContent("wrong_network");
});
expect(screen.getByText("Wrong network")).toBeInTheDocument();
});
it("fires an error toast on no_wallet", async () => {
(freighter.isFreighterConnected as jest.Mock).mockResolvedValue(false);
renderWithProviders();
await act(async () => {
screen.getByRole("button", { name: "Connect" }).click();
});
await waitFor(() => {
expect(screen.getByTestId("wallet-state")).toHaveTextContent("no_wallet");
});
expect(screen.getByText("No wallet")).toBeInTheDocument();
});
});
// ── 5. localStorage rehydration preserved ─────────────────────────────────────
describe("localStorage rehydration", () => {
it("rehydrates a persisted connected snapshot", async () => {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
version: 1,
state: WALLET_STATES_DIRECT.CONNECTED,
address: "GABC...XYZ123",
network: "public",
})
);
renderWithProviders();
await waitFor(() => {
expect(screen.getByTestId("wallet-state")).toHaveTextContent("connected");
});
});
it("disconnect() clears the persisted snapshot", async () => {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
version: 1,
state: WALLET_STATES_DIRECT.CONNECTED,
address: "GABC...XYZ123",
network: "public",
})
);
renderWithProviders();
await waitFor(() => {
expect(screen.getByTestId("wallet-state")).toHaveTextContent("connected");
});
await act(async () => {
screen.getByRole("button", { name: "Disconnect" }).click();
});
expect(screen.getByTestId("wallet-state")).toHaveTextContent("disconnected");
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
});
});
// ── 6. No legacy WalletContext-only identifiers exported from canonical source ─
describe("Canonical source does not leak legacy identifiers", () => {
it("does not export walletState (legacy alias)", () => {
// The canonical hook returns `state`, not `walletState`
renderWithProviders();
const wallet = screen.getByTestId("wallet-state");
// If hook returned walletState the probe would show nothing for `wallet.state`
// but the probe explicitly destructures `state`, so a non-empty value confirms it
expect(wallet.textContent).toBe("disconnected");
});
});