diff --git a/package.json b/package.json index 20067fa2c5..579b02ef5a 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "semver": "^7.5.2" }, "devDependencies": { + "@faker-js/faker": "^8.1.0", "@next/bundle-analyzer": "^13.1.1", "@openzeppelin/contracts": "^4.9.2", "@safe-global/safe-core-sdk-types": "^1.9.1", diff --git a/src/components/common/AddressInput/index.test.tsx b/src/components/common/AddressInput/index.test.tsx index 4a87f46d5d..e5e0df9201 100644 --- a/src/components/common/AddressInput/index.test.tsx +++ b/src/components/common/AddressInput/index.test.tsx @@ -4,15 +4,16 @@ import { useForm, FormProvider } from 'react-hook-form' import AddressInput, { type AddressInputProps } from '.' import { useCurrentChain } from '@/hooks/useChains' import useNameResolver from '@/components/common/AddressInput/useNameResolver' +import { chainBuilder } from '@/tests/builders/chains' +import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' + +const mockChain = chainBuilder() + .with({ features: [FEATURES.DOMAIN_LOOKUP] }) + .build() // mock useCurrentChain jest.mock('@/hooks/useChains', () => ({ - useCurrentChain: jest.fn(() => ({ - shortName: 'gor', - chainId: '5', - chainName: 'Goerli', - features: ['DOMAIN_LOOKUP'], - })), + useCurrentChain: jest.fn(() => mockChain), })) // mock useNameResolver @@ -92,7 +93,7 @@ describe('AddressInput tests', () => { ) act(() => { - fireEvent.change(input, { target: { value: 'gor:0x123' } }) + fireEvent.change(input, { target: { value: `${mockChain.shortName}:0x123` } }) jest.advanceTimersByTime(1000) }) @@ -103,14 +104,14 @@ describe('AddressInput tests', () => { const { input, utils } = setup('', (val) => `${val} is wrong`) act(() => { - fireEvent.change(input, { target: { value: `gor:${TEST_ADDRESS_A}` } }) + fireEvent.change(input, { target: { value: `${mockChain.shortName}:${TEST_ADDRESS_A}` } }) jest.advanceTimersByTime(1000) }) await waitFor(() => expect(utils.getByLabelText(`${TEST_ADDRESS_A} is wrong`, { exact: false })).toBeDefined()) act(() => { - fireEvent.change(input, { target: { value: `gor:${TEST_ADDRESS_B}` } }) + fireEvent.change(input, { target: { value: `${mockChain.shortName}:${TEST_ADDRESS_B}` } }) jest.advanceTimersByTime(1000) }) @@ -126,7 +127,7 @@ describe('AddressInput tests', () => { }) act(() => { - fireEvent.change(input, { target: { value: `gor:${TEST_ADDRESS_A}` } }) + fireEvent.change(input, { target: { value: `${mockChain.shortName}:${TEST_ADDRESS_A}` } }) jest.advanceTimersByTime(1000) }) @@ -195,17 +196,13 @@ describe('AddressInput tests', () => { }) it('should not show the adornment prefix when the value contains correct prefix', async () => { - ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ - shortName: 'gor', - chainId: '5', - chainName: 'Goerli', - features: [], - })) + const mockChain = chainBuilder().with({ features: [] }).build() + ;(useCurrentChain as jest.Mock).mockImplementation(() => mockChain) - const { input } = setup(`gor:${TEST_ADDRESS_A}`) + const { input } = setup(`${mockChain.shortName}:${TEST_ADDRESS_A}`) act(() => { - fireEvent.change(input, { target: { value: `gor:${TEST_ADDRESS_B}` } }) + fireEvent.change(input, { target: { value: `${mockChain.shortName}:${TEST_ADDRESS_B}` } }) }) await waitFor(() => expect(input.previousElementSibling?.textContent).toBe('')) diff --git a/src/components/common/CheckWallet/index.test.tsx b/src/components/common/CheckWallet/index.test.tsx index bb7ae1c468..c10a0d9dd7 100644 --- a/src/components/common/CheckWallet/index.test.tsx +++ b/src/components/common/CheckWallet/index.test.tsx @@ -3,6 +3,7 @@ import CheckWallet from '.' import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import useWallet from '@/hooks/wallets/useWallet' +import { chainBuilder } from '@/tests/builders/chains' // mock useWallet jest.mock('@/hooks/wallets/useWallet', () => ({ @@ -27,9 +28,7 @@ jest.mock('@/hooks/useIsOnlySpendingLimitBeneficiary', () => ({ // mock useCurrentChain jest.mock('@/hooks/useChains', () => ({ __esModule: true, - useCurrentChain: jest.fn(() => ({ - chainName: 'Optimism', - })), + useCurrentChain: jest.fn(() => chainBuilder().build()), })) const renderButton = () => render({(isOk) => }) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts index 1ad617b11a..b1f29b0991 100644 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts +++ b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts @@ -9,13 +9,13 @@ import * as txMonitor from '@/services/tx/txMonitor' import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers' import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { chainBuilder } from '@/tests/builders/chains' import { BigNumber } from '@ethersproject/bignumber' import { waitFor } from '@testing-library/react' import type Safe from '@safe-global/safe-core-sdk' import { hexZeroPad } from 'ethers/lib/utils' import type CompatibilityFallbackHandlerEthersContract from '@safe-global/safe-ethers-lib/dist/src/contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract' -import { FEATURES } from '@/utils/chains' +import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' import * as gasPrice from '@/hooks/useGasPrice' const mockSafeInfo = { @@ -44,10 +44,7 @@ describe('useSafeCreation', () => { beforeEach(() => { jest.resetAllMocks() - const mockChain = { - chainId: '4', - features: [], - } as unknown as ChainInfo + const mockChain = chainBuilder().with({ features: [] }).build() jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => mockProvider) @@ -89,12 +86,10 @@ describe('useSafeCreation', () => { false, ]) - jest.spyOn(chain, 'useCurrentChain').mockImplementation( - () => - ({ - chainId: '4', - features: [FEATURES.EIP1559], - } as unknown as ChainInfo), + jest.spyOn(chain, 'useCurrentChain').mockImplementation(() => + chainBuilder() + .with({ features: [FEATURES.EIP1559] }) + .build(), ) jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx index 655e32e8b6..f76a11f0a5 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx @@ -1,5 +1,5 @@ import { hexlify, hexZeroPad, toUtf8Bytes } from 'ethers/lib/utils' -import type { ChainInfo, SafeInfo, SafeMessage, SafeMessageListPage } from '@safe-global/safe-gateway-typescript-sdk' +import type { SafeInfo, SafeMessage, SafeMessageListPage } from '@safe-global/safe-gateway-typescript-sdk' import { SafeMessageListItemType } from '@safe-global/safe-gateway-typescript-sdk' import SignMessage from './SignMessage' @@ -17,6 +17,7 @@ import type { EIP1193Provider, WalletState, AppState, OnboardAPI } from '@web3-o import { generateSafeMessageHash } from '@/utils/safe-messages' import { getSafeMessage } from '@safe-global/safe-gateway-typescript-sdk' import useSafeMessages from '@/hooks/messages/useSafeMessages' +import { chainBuilder } from '@/tests/builders/chains' jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), @@ -400,7 +401,7 @@ describe('SignMessage', () => { jest.spyOn(onboard, 'default').mockReturnValue(mockOnboard) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) jest.spyOn(useIsWrongChainHook, 'default').mockImplementation(() => true) - jest.spyOn(useChainsHook, 'useCurrentChain').mockReturnValue({ chainName: 'Goerli' } as ChainInfo) + jest.spyOn(useChainsHook, 'useCurrentChain').mockReturnValue(chainBuilder().build()) const { getByText } = render( { + const mockChain = chainBuilder() + // @ts-expect-error - using local FEATURES enum + .with({ features: [FEATURES.RELAYING] }) + .build() beforeEach(() => { - jest - .spyOn(useChains, 'useCurrentChain') - .mockReturnValue({ chainId: '5', features: FEATURES.RELAYING } as unknown as ChainInfo) + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue(mockChain) jest.spyOn(useSafeInfo, 'default').mockReturnValue({ safe: { txHistoryTag: '0', @@ -41,7 +43,7 @@ describe('fetch remaining relays hooks', () => { global.fetch = jest.fn() const mockFetch = jest.spyOn(global, 'fetch') - const url = `${SAFE_RELAY_SERVICE_URL}/5/${SAFE_ADDRESS}` + const url = `${SAFE_RELAY_SERVICE_URL}/${mockChain.chainId}/${SAFE_ADDRESS}` renderHook(() => useRelaysBySafe()) expect(mockFetch).toHaveBeenCalledTimes(1) @@ -49,7 +51,7 @@ describe('fetch remaining relays hooks', () => { }) it('should not do a network request if chain does not support relay', () => { - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ chainId: '5', features: [] } as unknown as ChainInfo) + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue(chainBuilder().with({ features: [] }).build()) global.fetch = jest.fn() const mockFetch = jest.spyOn(global, 'fetch') @@ -151,7 +153,7 @@ describe('fetch remaining relays hooks', () => { }) it('should not do a network request if chain does not support relay', () => { - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ chainId: '5', features: [] } as unknown as ChainInfo) + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue(chainBuilder().with({ features: [] }).build()) global.fetch = jest .fn() .mockResolvedValue({ diff --git a/src/hooks/useMnemonicName/useMnemonicName.test.ts b/src/hooks/useMnemonicName/useMnemonicName.test.ts index 9a126982e7..a47b206aff 100644 --- a/src/hooks/useMnemonicName/useMnemonicName.test.ts +++ b/src/hooks/useMnemonicName/useMnemonicName.test.ts @@ -1,11 +1,12 @@ import { getRandomName, useMnemonicName, useMnemonicSafeName } from '.' import { renderHook } from '@/tests/test-utils' +import { chainBuilder } from '@/tests/builders/chains' + +const mockChain = chainBuilder().build() // Mock useCurrentChain hook jest.mock('@/hooks/useChains', () => ({ - useCurrentChain: () => ({ - chainName: 'Rinkeby', - }), + useCurrentChain: () => mockChain, })) describe('useMnemonicName tests', () => { @@ -37,6 +38,7 @@ describe('useMnemonicName tests', () => { it('should return a random safe name', () => { const { result } = renderHook(() => useMnemonicSafeName()) - expect(result.current).toMatch(/^[a-z-]+-rinkeby-safe$/) + const regex = new RegExp(`^[a-z-]+-${mockChain.chainName.toLowerCase()}-safe$`) + expect(result.current).toMatch(regex) }) }) diff --git a/src/tests/Builder.ts b/src/tests/Builder.ts new file mode 100644 index 0000000000..139302194a --- /dev/null +++ b/src/tests/Builder.ts @@ -0,0 +1,30 @@ +export interface IBuilder { + with(override: Partial): IBuilder + + build(): T +} + +export class Builder implements IBuilder { + private constructor(private target: Partial) {} + + /** + * Returns a new {@link Builder} with the property {@link key} set to {@link value}. + * + * @param override - the override value to apply + */ + with(override: Partial): IBuilder { + const target: Partial = { ...this.target, ...override } + return new Builder(target) + } + + /** + * Returns an instance of T with the values that were set so far + */ + build(): T { + return this.target as T + } + + public static new(): IBuilder { + return new Builder({}) + } +} diff --git a/src/tests/builders/chains.ts b/src/tests/builders/chains.ts new file mode 100644 index 0000000000..e4327ba112 --- /dev/null +++ b/src/tests/builders/chains.ts @@ -0,0 +1,121 @@ +import { faker } from '@faker-js/faker' +import { RPC_AUTHENTICATION, GAS_PRICE_TYPE } from '@safe-global/safe-gateway-typescript-sdk' +import type { + BlockExplorerUriTemplate, + ChainInfo, + GasPriceFixed, + GasPriceFixedEIP1559, + GasPriceOracle, + GasPriceUnknown, + NativeCurrency, + RpcUri, + Theme, +} from '@safe-global/safe-gateway-typescript-sdk' + +import { Builder } from '@/tests/Builder' +import { FEATURES } from '@/utils/chains' +import { generateRandomArray } from './utils' +import type { IBuilder } from '@/tests/Builder' +import type useChains from '@/hooks/useChains' + +const rpcUriBuilder = (): IBuilder => { + return Builder.new().with({ + authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, + value: faker.internet.url({ appendSlash: false }), + }) +} + +const blockExplorerUriTemplateBuilder = (): IBuilder => { + return Builder.new().with({ + address: faker.finance.ethereumAddress(), + txHash: faker.string.hexadecimal(), + api: faker.internet.url({ appendSlash: false }), + }) +} + +const nativeCurrencyBuilder = (): IBuilder => { + return Builder.new().with({ + name: faker.finance.currencyName(), + symbol: faker.finance.currencySymbol(), + decimals: 18, + logoUri: faker.internet.url({ appendSlash: false }), + }) +} + +const themeBuilder = (): IBuilder => { + return Builder.new().with({ + textColor: faker.color.rgb(), + backgroundColor: faker.color.rgb(), + }) +} + +const gasPriceFixedBuilder = (): IBuilder => { + return Builder.new().with({ + type: GAS_PRICE_TYPE.FIXED, + weiValue: faker.string.numeric(), + }) +} + +const gasPriceFixedEIP1559Builder = (): IBuilder => { + return Builder.new().with({ + type: GAS_PRICE_TYPE.FIXED_1559, + maxFeePerGas: faker.string.numeric(), + maxPriorityFeePerGas: faker.string.numeric(), + }) +} + +const gasPriceOracleBuilder = (): IBuilder => { + return Builder.new().with({ + type: GAS_PRICE_TYPE.ORACLE, + uri: faker.internet.url({ appendSlash: false }), + gasParameter: faker.word.sample(), + gweiFactor: faker.string.numeric(), + }) +} + +const gasPriceOracleUnknownBuilder = (): IBuilder => { + return Builder.new().with({ + type: GAS_PRICE_TYPE.UNKNOWN, + }) +} + +const getRandomGasPriceBuilder = () => { + const gasPriceBuilders = [ + gasPriceFixedBuilder(), + gasPriceFixedEIP1559Builder(), + gasPriceOracleBuilder(), + gasPriceOracleUnknownBuilder(), + ] + + const randomIndex = Math.floor(Math.random() * gasPriceBuilders.length) + return gasPriceBuilders[randomIndex] +} + +export const chainBuilder = (): IBuilder => { + return Builder.new().with({ + chainId: faker.string.numeric(), + chainName: faker.word.sample(), + description: faker.word.words(), + l2: faker.datatype.boolean(), + shortName: faker.word.sample(), + rpcUri: rpcUriBuilder().build(), + safeAppsRpcUri: rpcUriBuilder().build(), + publicRpcUri: rpcUriBuilder().build(), + blockExplorerUriTemplate: blockExplorerUriTemplateBuilder().build(), + nativeCurrency: nativeCurrencyBuilder().build(), + transactionService: faker.internet.url({ appendSlash: false }), + theme: themeBuilder().build(), + gasPrice: generateRandomArray(() => getRandomGasPriceBuilder().build(), { min: 1, max: 4 }), + ensRegistryAddress: faker.finance.ethereumAddress(), + disabledWallets: generateRandomArray(() => faker.word.sample(), { min: 1, max: 10 }), + // @ts-expect-error - we are using a local FEATURES enum + features: generateRandomArray(() => faker.helpers.enumValue(FEATURES), { min: 1, max: 10 }), + }) +} + +export const useChainsBuilder = (): IBuilder> => { + return Builder.new>().with({ + configs: generateRandomArray(() => chainBuilder().build(), { min: 1, max: 25 }), + loading: false, + }) +} diff --git a/src/tests/builders/utils.ts b/src/tests/builders/utils.ts new file mode 100644 index 0000000000..5892653303 --- /dev/null +++ b/src/tests/builders/utils.ts @@ -0,0 +1,6 @@ +import { faker } from '@faker-js/faker' +import type { NumberModule } from '@faker-js/faker' + +export const generateRandomArray = (generator: () => T, options?: Parameters[0]): Array => { + return Array.from({ length: faker.number.int(options) }, generator) +} diff --git a/yarn.lock b/yarn.lock index 5d610da99d..6b66bcaa40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2263,6 +2263,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@faker-js/faker@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.1.0.tgz#e14896f1c57af2495e341dc4c7bf04125c8aeafd" + integrity sha512-38DT60rumHfBYynif3lmtxMqMqmsOQIxQgEuPZxCk2yUYN0eqWpTACgxi0VpidvsJB8CRxCpvP7B3anK85FjtQ== + "@firebase/analytics-compat@0.2.6": version "0.2.6" resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz#50063978c42f13eb800e037e96ac4b17236841f4"