Skip to content

Commit 9a36491

Browse files
test(wallet): add full useStellarWallet hook test coverage (#118)
Extend the useStellarWallet tests to cover every acceptance criterion from #68: Freighter connect (success, extension missing, not unlocked), Lobstr connect (success, not installed, signing cancelled), import (valid and invalid secret keys), disconnectWallet clearing the vault, and balance/token refresh success and failure paths. 🤖 Generated with Codebuff Co-authored-by: Codebuff <noreply@codebuff.com>
1 parent c464736 commit 9a36491

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

src/__tests__/useStellarWallet.test.tsx

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,46 @@ describe('useStellarWallet connect flow', () => {
260260

261261
expectConnected(IN_APP_PK, 'inapp', '5', '75', '25');
262262
});
263+
264+
it('Freighter: connects successfully and records the wallet type', async () => {
265+
(globalThis as { window?: unknown }).window = {
266+
freighter: {
267+
isConnected: jest.fn().mockResolvedValue(true),
268+
getPublicKey: jest.fn().mockResolvedValue(FREIGHTER_PK),
269+
signTransaction: jest.fn(),
270+
},
271+
};
272+
tree = await renderProbe();
273+
await act(async () => {
274+
await hook.connectFreighter();
275+
});
276+
expectConnected(FREIGHTER_PK, 'freighter', '123.45', '75', '25');
277+
expect(hook.error).toBeNull();
278+
});
279+
280+
it('Lobstr: connects successfully and extracts the key from the signed challenge', async () => {
281+
tree = await renderProbe();
282+
await act(async () => {
283+
await hook.connectLobstr();
284+
});
285+
expect(openLobstrForSigning).toHaveBeenCalled();
286+
expectConnected('GLOBSTRUSERKEY', 'lobstr', '123.45', '75', '25');
287+
expect(hook.error).toBeNull();
288+
});
289+
290+
it('import: valid secret key connects and persists to the vault', async () => {
291+
tree = await renderProbe();
292+
let result: { publicKey: string } | undefined;
293+
await act(async () => {
294+
result = await hook.importWallet(' SVALIDIMPORTSECRET ');
295+
});
296+
expect(result).toEqual({ publicKey: IMPORT_PK });
297+
// Secret is trimmed before validation/derivation.
298+
expect(isValidSecretKey).toHaveBeenCalledWith('SVALIDIMPORTSECRET');
299+
expectConnected(IMPORT_PK, 'inapp', '123.45', '75', '25');
300+
expect(getInAppSecret(IMPORT_PK)).toBe('SVALIDIMPORTSECRET');
301+
expect(hook.error).toBeNull();
302+
});
263303
});
264304

265305
describe('balance fetch failure rolls back (all wallet paths)', () => {
@@ -381,6 +421,90 @@ describe('useStellarWallet connect flow', () => {
381421
});
382422
});
383423

424+
describe('connection errors (per wallet path)', () => {
425+
it('Freighter: reports when the extension is not detected', async () => {
426+
delete (globalThis as { window?: unknown }).window;
427+
tree = await renderProbe();
428+
await act(async () => {
429+
await hook.connectFreighter();
430+
});
431+
expect(hook.error).toBe('Freighter extension not detected');
432+
expectDisconnected();
433+
});
434+
435+
it('Freighter: asks the user to unlock the extension when not connected', async () => {
436+
(globalThis as { window?: unknown }).window = {
437+
freighter: {
438+
isConnected: jest.fn().mockResolvedValue(false),
439+
getPublicKey: jest.fn(),
440+
signTransaction: jest.fn(),
441+
},
442+
};
443+
tree = await renderProbe();
444+
await act(async () => {
445+
await hook.connectFreighter();
446+
});
447+
expect(hook.error).toBe('Please unlock Freighter first');
448+
expect(getBalance).not.toHaveBeenCalled();
449+
expectDisconnected();
450+
});
451+
452+
it('Lobstr: surfaces a clear error when the app is not installed', async () => {
453+
isLobstrInstalled.mockResolvedValue(false);
454+
tree = await renderProbe();
455+
await act(async () => {
456+
await hook.connectLobstr();
457+
});
458+
expect(hook.error).toMatch(/Lobstr is not installed/i);
459+
expect(openLobstrForSigning).not.toHaveBeenCalled();
460+
expectDisconnected();
461+
});
462+
463+
it('Lobstr: surfaces an error when signing is cancelled', async () => {
464+
openLobstrForSigning.mockRejectedValue(
465+
new Error('Lobstr signing was cancelled'),
466+
);
467+
tree = await renderProbe();
468+
await act(async () => {
469+
await hook.connectLobstr();
470+
});
471+
expect(hook.error).toBe('Lobstr signing was cancelled');
472+
expect(getBalance).not.toHaveBeenCalled();
473+
expectDisconnected();
474+
});
475+
476+
it('import: invalid secret key is rejected without persisting', async () => {
477+
isValidSecretKey.mockReturnValue(false);
478+
tree = await renderProbe();
479+
let result: { publicKey: string } | undefined;
480+
await act(async () => {
481+
result = await hook.importWallet('SBADKEY');
482+
});
483+
expect(result).toBeUndefined();
484+
expect(hook.error).toBe('Invalid secret key');
485+
expect(getPublicKeyFromSecret).not.toHaveBeenCalled();
486+
expect(getBalance).not.toHaveBeenCalled();
487+
expectDisconnected();
488+
expect(getInAppSecret(IMPORT_PK)).toBeNull();
489+
});
490+
});
491+
492+
describe('disconnectWallet', () => {
493+
it('clears the vault entry and resets the store', async () => {
494+
tree = await renderProbe();
495+
await act(async () => {
496+
await hook.createInAppWallet();
497+
});
498+
expect(getInAppSecret(IN_APP_PK)).toBe(IN_APP_SECRET);
499+
500+
await act(async () => {
501+
hook.disconnectWallet();
502+
});
503+
expect(getInAppSecret(IN_APP_PK)).toBeNull();
504+
expectDisconnected();
505+
});
506+
});
507+
384508
describe('refreshes after connect are fail-safe', () => {
385509
it('keeps the last known balance when a later refresh fails', async () => {
386510
tree = await renderProbe();
@@ -400,6 +524,54 @@ describe('useStellarWallet connect flow', () => {
400524
expect(useWalletStore.getState().isConnected).toBe(true);
401525
expect(hook.error).toBe('offline blip');
402526
});
527+
528+
it('refreshBalance updates the store with a fresh balance', async () => {
529+
tree = await renderProbe();
530+
await act(async () => {
531+
await hook.createInAppWallet();
532+
});
533+
expect(useWalletStore.getState().balance).toBe('123.45');
534+
535+
getBalance.mockResolvedValue('999.99');
536+
await act(async () => {
537+
await hook.refreshBalance();
538+
});
539+
expect(useWalletStore.getState().balance).toBe('999.99');
540+
expect(hook.error).toBeNull();
541+
});
542+
543+
it('refreshEcoBalance and refreshUsdcBalance update their token balances', async () => {
544+
tree = await renderProbe();
545+
await act(async () => {
546+
await hook.createInAppWallet();
547+
});
548+
549+
getTokenBalance.mockImplementation((_pk: string, assetCode: string) =>
550+
Promise.resolve(assetCode === 'USDC' ? '42' : '13'),
551+
);
552+
await act(async () => {
553+
await hook.refreshEcoBalance();
554+
await hook.refreshUsdcBalance();
555+
});
556+
expect(useWalletStore.getState().ecoBalance).toBe('13');
557+
expect(useWalletStore.getState().usdcBalance).toBe('42');
558+
expect(hook.error).toBeNull();
559+
});
560+
561+
it('keeps the last known token balance when a token refresh fails', async () => {
562+
tree = await renderProbe();
563+
await act(async () => {
564+
await hook.createInAppWallet();
565+
});
566+
expect(useWalletStore.getState().ecoBalance).toBe('75');
567+
568+
getTokenBalance.mockRejectedValue(new Error('token horizon down'));
569+
await act(async () => {
570+
await hook.refreshEcoBalance();
571+
});
572+
expect(useWalletStore.getState().ecoBalance).toBe('75');
573+
expect(hook.error).toBe('token horizon down');
574+
});
403575
});
404576
});
405577

0 commit comments

Comments
 (0)