Skip to content

Commit

Permalink
Redesigned useNetwork to use chrome storage instead of zustand
Browse files Browse the repository at this point in the history
Closes #655
  • Loading branch information
tombeckenham committed Mar 7, 2025
1 parent e6e77da commit 536bcde
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 199 deletions.
15 changes: 0 additions & 15 deletions src/ui/hooks/__tests__/useCoinHook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { act } from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { useCoinStore } from '@/ui/stores/coinStore';
import { useNetworkStore } from '@/ui/stores/networkStore';
import { useProfileStore } from '@/ui/stores/profileStore';
import { useWallet, useWalletLoaded } from '@/ui/utils/WalletContext';

Expand All @@ -19,20 +18,6 @@ vi.mock('react', async () => {
};
});

// Mock all stores first
vi.mock('@/ui/stores/networkStore', () => ({
useNetworkStore: vi.fn((selector) =>
selector({
currentNetwork: 'mainnet',
developerMode: false,
emulatorModeOn: false,
setNetwork: vi.fn(),
setDeveloperMode: vi.fn(),
setEmulatorModeOn: vi.fn(),
})
),
}));

vi.mock('@/ui/stores/profileStore', () => ({
useProfileStore: vi.fn((selector) =>
selector({
Expand Down
246 changes: 204 additions & 42 deletions src/ui/hooks/__tests__/useNetworkHook.test.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,226 @@
import { act, useState, useEffect } from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { useNetworkStore } from '@/ui/stores/networkStore';
import { useWallet, useWalletLoaded } from '@/ui/utils/WalletContext';

import { useNetworks } from '../useNetworkHook';
import { useNetwork } from '../useNetworkHook';

// Mock React
vi.mock('react', async () => {
const actual = await vi.importActual('react');
return {
...actual,
useCallback: vi.fn().mockImplementation((fn) => fn),
useEffect: vi.fn().mockImplementation((fn) => {
const cleanup = fn();
return cleanup;
}),
useState: vi.fn().mockImplementation((initialValue) => [initialValue, vi.fn()]),
};
});

// Mock the store
vi.mock('@/ui/stores/networkStore', () => ({
useNetworkStore: vi.fn((selector) =>
selector({
currentNetwork: 'mainnet',
developerMode: false,
emulatorModeOn: false,
setNetwork: vi.fn(),
setDeveloperMode: vi.fn(),
setEmulatorModeOn: vi.fn(),
})
),
}));

// Mock the wallet context
vi.mock('@/ui/utils/WalletContext', () => ({
useWalletLoaded: vi.fn().mockReturnValue(true),
useWallet: vi.fn().mockReturnValue({
getNetwork: vi.fn().mockResolvedValue('mainnet'),
}),
}));
// Mock chrome API
const mockChromeStorage = {
local: {
get: vi.fn(),
},
onChanged: {
addListener: vi.fn(),
removeListener: vi.fn(),
},
};

vi.stubGlobal('chrome', {
storage: mockChromeStorage,
});

describe('useNetworkHook', () => {
let mockSetNetwork: ReturnType<typeof vi.fn>;
let setNetworkMock: ReturnType<typeof vi.fn>;
let setDeveloperModeMock: ReturnType<typeof vi.fn>;
let setEmulatorModeOnMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
mockSetNetwork = vi.fn();
vi.mocked(useNetworkStore).mockImplementation((selector) =>
selector({
currentNetwork: 'mainnet',
developerMode: false,
emulatorModeOn: false,
setNetwork: mockSetNetwork,
setDeveloperMode: vi.fn(),
setEmulatorModeOn: vi.fn(),
})
vi.clearAllMocks();

setNetworkMock = vi.fn();
setDeveloperModeMock = vi.fn();
setEmulatorModeOnMock = vi.fn();

// Mock useState to return our mock setters
vi.mocked(useState).mockImplementationOnce(() => ['mainnet', setNetworkMock]);
vi.mocked(useState).mockImplementationOnce(() => [false, setDeveloperModeMock]);
vi.mocked(useState).mockImplementationOnce(() => [false, setEmulatorModeOnMock]);

// Reset chrome storage mock with default values
mockChromeStorage.local.get.mockImplementation((key) => {
if (key === 'developerMode') {
return Promise.resolve({ developerMode: false });
}
if (key === 'emulatorMode') {
return Promise.resolve({ emulatorMode: false });
}
if (key === 'userWallets') {
return Promise.resolve({ userWallets: { network: 'mainnet' } });
}
return Promise.resolve({});
});
});

it('should initialize with default values', () => {
const { network, developerMode, emulatorModeOn } = useNetwork();

expect(network).toBe('mainnet');
expect(developerMode).toBe(false);
expect(emulatorModeOn).toBe(false);
expect(mockChromeStorage.onChanged.addListener).toHaveBeenCalled();
});

it('should load initial data from storage', async () => {
// Mock storage returns for initial load
mockChromeStorage.local.get.mockImplementation((key) => {
if (key === 'developerMode') {
return Promise.resolve({ developerMode: true });
}
if (key === 'emulatorMode') {
return Promise.resolve({ emulatorMode: true });
}
if (key === 'userWallets') {
return Promise.resolve({ userWallets: { network: 'testnet' } });
}
return Promise.resolve({});
});

useNetwork();

// Wait for promises to resolve
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

expect(mockChromeStorage.local.get).toHaveBeenCalledWith('developerMode');
expect(mockChromeStorage.local.get).toHaveBeenCalledWith('emulatorMode');
expect(mockChromeStorage.local.get).toHaveBeenCalledWith('userWallets');
});

it('should handle storage changes for userWallets', () => {
useNetwork();

// Simulate storage change event for userWallets
const handleStorageChange = mockChromeStorage.onChanged.addListener.mock.calls[0][0];

handleStorageChange(
{
userWallets: {
newValue: { network: 'testnet', emulatorMode: true },
oldValue: { network: 'mainnet', emulatorMode: false },
},
},
'local'
);

expect(setNetworkMock).toHaveBeenCalledWith('testnet');
expect(setEmulatorModeOnMock).toHaveBeenCalledWith(true);
});

describe('fetchNetwork', () => {
it('should correctly identify network type', async () => {
const hook = useNetworks();
await hook.fetchNetwork();
it('should handle storage changes for developerMode', () => {
useNetwork();

// Simulate storage change event for developerMode
const handleStorageChange = mockChromeStorage.onChanged.addListener.mock.calls[0][0];

handleStorageChange(
{
developerMode: {
newValue: true,
oldValue: false,
},
},
'local'
);

expect(setDeveloperModeMock).toHaveBeenCalledWith(true);
});

it('should clean up listeners on unmount', () => {
useNetwork();

// Get the cleanup function from the useEffect mock
const cleanupFn = vi.mocked(useEffect).mock.calls[0][0]();

if (cleanupFn) {
// Call the cleanup function
cleanupFn();

expect(mockChromeStorage.onChanged.removeListener).toHaveBeenCalled();
}
});

expect(mockSetNetwork).toHaveBeenCalledWith('mainnet');
it('should not update state if component is unmounted', async () => {
// Create a flag to simulate mounted state
let mounted = true;

// Override the useEffect implementation for this test only
vi.mocked(useEffect).mockImplementationOnce((fn) => {
fn();
return () => {
mounted = false;
};
});

// Override useState implementations to check mounted flag
vi.mocked(useState).mockReset();
vi.mocked(useState).mockImplementationOnce(() => [
'mainnet',
(val) => {
if (mounted) setNetworkMock(val);
},
]);
vi.mocked(useState).mockImplementationOnce(() => [
false,
(val) => {
if (mounted) setDeveloperModeMock(val);
},
]);
vi.mocked(useState).mockImplementationOnce(() => [
false,
(val) => {
if (mounted) setEmulatorModeOnMock(val);
},
]);

// Mock storage to return delayed promises
mockChromeStorage.local.get.mockImplementation((key) => {
return new Promise((resolve) => {
setTimeout(() => {
if (key === 'developerMode') {
resolve({ developerMode: true });
} else if (key === 'emulatorMode') {
resolve({ emulatorMode: true });
} else if (key === 'userWallets') {
resolve({ userWallets: { network: 'testnet' } });
} else {
resolve({});
}
}, 10); // Small delay to ensure it happens after unmount
});
});

// Call the hook
useNetwork();

// Clear mocks before unmounting
setNetworkMock.mockClear();
setDeveloperModeMock.mockClear();
setEmulatorModeOnMock.mockClear();

// Simulate unmounting
mounted = false;

// Wait for delayed promises to resolve
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
});

// State setters should not be called after unmount
expect(setNetworkMock).not.toHaveBeenCalled();
expect(setDeveloperModeMock).not.toHaveBeenCalled();
expect(setEmulatorModeOnMock).not.toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion src/ui/hooks/__tests__/useProfileHook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ vi.mock('react', async () => {
...actual,
useEffect: vi.fn((fn) => fn()),
useCallback: vi.fn((fn) => fn),
useState: vi.fn((initialValue) => [initialValue, vi.fn()]),
};
});

import { describe, it, expect, vi, beforeEach } from 'vitest';

import emojiList from '@/background/utils/emoji.json';
import { useProfileStore } from '@/ui/stores/profileStore';
import { useWallet, useWalletLoaded } from '@/ui/utils/WalletContext';

import { useProfiles } from '../useProfileHook';

Expand Down
12 changes: 1 addition & 11 deletions src/ui/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,23 @@ import { useCallback } from 'react';
import { useWalletLoaded } from '../utils/WalletContext';

import { useCoins } from './useCoinHook';
import { useNetworks } from './useNetworkHook';
import { useProfiles } from './useProfileHook';

export const useInitHook = () => {
const walletLoaded = useWalletLoaded();
const { fetchProfileData, freshUserWallet, fetchUserWallet } = useProfiles();
const { fetchNetwork } = useNetworks();
const { refreshCoinData } = useCoins();

const initializeStore = useCallback(async () => {
if (!walletLoaded) {
return;
}

await fetchNetwork();
await fetchProfileData();
await freshUserWallet();
await fetchUserWallet();
await refreshCoinData();
}, [
fetchNetwork,
fetchProfileData,
freshUserWallet,
fetchUserWallet,
refreshCoinData,
walletLoaded,
]);
}, [fetchProfileData, freshUserWallet, fetchUserWallet, refreshCoinData, walletLoaded]);

return { initializeStore };
};
Loading

0 comments on commit 536bcde

Please sign in to comment.