From 1a9a0aa9c73529baabb5bdd66c1a36e02d1b4a01 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:19:09 -0300 Subject: [PATCH 1/3] test: add utility and env test coverage (Tier 1) Add test safety net before dependency updates. New files: - src/test-utils.tsx: renderWithProviders, createMockWeb3Status, createMockChain - src/utils/strings.test.ts: truncateStringInTheMiddle and getTruncatedHash (13 tests) - src/utils/getExplorerLink.test.ts: getExplorerLink (6 tests) - src/utils/address.test.ts: isNativeToken (5 tests) - src/env.test.ts: Zod-validated env schema (9 tests) - .env.test: Vitest environment variables for required PUBLIC_ fields --- .env.test | 7 +++ src/env.test.ts | 53 +++++++++++++++++++++ src/test-utils.tsx | 52 ++++++++++++++++++++ src/utils/address.test.ts | 35 ++++++++++++++ src/utils/getExplorerLink.test.ts | 52 ++++++++++++++++++++ src/utils/strings.test.ts | 79 +++++++++++++++++++++++++++++++ 6 files changed, 278 insertions(+) create mode 100644 .env.test create mode 100644 src/env.test.ts create mode 100644 src/test-utils.tsx create mode 100644 src/utils/address.test.ts create mode 100644 src/utils/getExplorerLink.test.ts create mode 100644 src/utils/strings.test.ts diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..f4bf82d2 --- /dev/null +++ b/.env.test @@ -0,0 +1,7 @@ +# Test environment variables for Vitest +PUBLIC_APP_NAME=dAppBooster Test +PUBLIC_NATIVE_TOKEN_ADDRESS=0x0000000000000000000000000000000000000000 +PUBLIC_WALLETCONNECT_PROJECT_ID=test-project-id +PUBLIC_SUBGRAPHS_API_KEY=test-api-key +PUBLIC_SUBGRAPHS_CHAINS_RESOURCE_IDS=1:test:test-resource-id +PUBLIC_SUBGRAPHS_ENVIRONMENT=production diff --git a/src/env.test.ts b/src/env.test.ts new file mode 100644 index 00000000..648db0f6 --- /dev/null +++ b/src/env.test.ts @@ -0,0 +1,53 @@ +import { zeroAddress } from 'viem' +import { describe, expect, it } from 'vitest' + +// env.ts reads import.meta.env at module load time. +// Vitest loads .env.test automatically for the "test" mode, +// so PUBLIC_APP_NAME, PUBLIC_SUBGRAPHS_*, etc. are set via .env.test. +import { env } from './env' + +describe('env', () => { + it('exposes PUBLIC_APP_NAME from test env', () => { + expect(env.PUBLIC_APP_NAME).toBe('dAppBooster Test') + }) + + it('defaults PUBLIC_NATIVE_TOKEN_ADDRESS to zero address when not set', () => { + // .env.test sets it to the zero address explicitly + expect(env.PUBLIC_NATIVE_TOKEN_ADDRESS).toBe(zeroAddress.toLowerCase()) + }) + + it('lowercases PUBLIC_NATIVE_TOKEN_ADDRESS', () => { + expect(env.PUBLIC_NATIVE_TOKEN_ADDRESS).toBe(env.PUBLIC_NATIVE_TOKEN_ADDRESS.toLowerCase()) + }) + + it('defaults PUBLIC_ENABLE_PORTO to true', () => { + expect(env.PUBLIC_ENABLE_PORTO).toBe(true) + }) + + it('defaults PUBLIC_USE_DEFAULT_TOKENS to true', () => { + expect(env.PUBLIC_USE_DEFAULT_TOKENS).toBe(true) + }) + + it('defaults PUBLIC_INCLUDE_TESTNETS to true', () => { + expect(env.PUBLIC_INCLUDE_TESTNETS).toBe(true) + }) + + it('defaults PUBLIC_SUBGRAPHS_ENVIRONMENT to production', () => { + expect(env.PUBLIC_SUBGRAPHS_ENVIRONMENT).toBe('production') + }) + + it('exposes PUBLIC_SUBGRAPHS_API_KEY from test env', () => { + expect(env.PUBLIC_SUBGRAPHS_API_KEY).toBe('test-api-key') + }) + + it('exposes PUBLIC_WALLETCONNECT_PROJECT_ID with empty string default', () => { + // .env.test sets it to 'test-project-id' + expect(typeof env.PUBLIC_WALLETCONNECT_PROJECT_ID).toBe('string') + }) + + it('optional RPC vars are undefined when not set in test env', () => { + // None of the RPC vars are set in .env.test + expect(env.PUBLIC_RPC_MAINNET).toBeUndefined() + expect(env.PUBLIC_RPC_SEPOLIA).toBeUndefined() + }) +}) diff --git a/src/test-utils.tsx b/src/test-utils.tsx new file mode 100644 index 00000000..124d95c3 --- /dev/null +++ b/src/test-utils.tsx @@ -0,0 +1,52 @@ +import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react' +import { render } from '@testing-library/react' +import type { ReactNode } from 'react' +import type { Chain } from 'viem' + +const system = createSystem(defaultConfig) + +/** + * Wraps a component in the providers needed for most tests. + */ +export function renderWithProviders(ui: ReactNode) { + return render({ui}) +} + +/** + * Returns a minimal mock of the useWeb3Status return value. + * Pass overrides to test specific states. + */ +export function createMockWeb3Status(overrides?: Partial>) { + return { ..._mockShape(), ...overrides } +} + +function _mockShape() { + return { + address: undefined as `0x${string}` | undefined, + isConnected: false, + isConnecting: false, + isDisconnected: true, + chainId: undefined as number | undefined, + balance: undefined, + publicClient: undefined, + walletClient: undefined, + disconnect: () => {}, + switchChain: undefined, + } +} + +/** + * Returns a minimal valid viem Chain object for tests. + */ +export function createMockChain(overrides?: Partial): Chain { + return { + id: 1, + name: 'Mock Chain', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['https://mock.rpc.url'] } }, + blockExplorers: { + default: { name: 'MockExplorer', url: 'https://mock.explorer.url' }, + }, + ...overrides, + } as Chain +} diff --git a/src/utils/address.test.ts b/src/utils/address.test.ts new file mode 100644 index 00000000..6a49fd05 --- /dev/null +++ b/src/utils/address.test.ts @@ -0,0 +1,35 @@ +import { zeroAddress } from 'viem' +import { describe, expect, it, vi } from 'vitest' + +// Mock env before importing isNativeToken so the module sees the mock +vi.mock('@/src/env', () => ({ + env: { + PUBLIC_NATIVE_TOKEN_ADDRESS: zeroAddress.toLowerCase(), + }, +})) + +import { isNativeToken } from './address' + +describe('isNativeToken', () => { + it('returns true for the zero address (default native token)', () => { + expect(isNativeToken(zeroAddress)).toBe(true) + }) + + it('returns true for the zero address in lowercase', () => { + expect(isNativeToken(zeroAddress.toLowerCase())).toBe(true) + }) + + it('returns true for a checksummed zero address', () => { + // zeroAddress is already lowercase, but ensure uppercase hex still matches + expect(isNativeToken('0x0000000000000000000000000000000000000000')).toBe(true) + }) + + it('returns false for a regular ERC20 contract address', () => { + expect(isNativeToken('0x71C7656EC7ab88b098defB751B7401B5f6d8976F')).toBe(false) + }) + + it('comparison is case-insensitive', () => { + // Both upper and lower case should match the native token (zero address) + expect(isNativeToken('0X0000000000000000000000000000000000000000')).toBe(true) + }) +}) diff --git a/src/utils/getExplorerLink.test.ts b/src/utils/getExplorerLink.test.ts new file mode 100644 index 00000000..31398572 --- /dev/null +++ b/src/utils/getExplorerLink.test.ts @@ -0,0 +1,52 @@ +import { createMockChain } from '@/src/test-utils' +import type { Chain } from 'viem' +import { describe, expect, it } from 'vitest' +import { getExplorerLink } from './getExplorerLink' + +const chain = createMockChain() +// A valid address (40 hex chars after 0x) +const address = '0x71C7656EC7ab88b098defB751B7401B5f6d8976F' as const +// A valid tx hash (64 hex chars after 0x) +const txHash = '0xd85ef8c70dc31a4f8d5bf0331e1eac886935905f15d32e71b348df745cd38e19' as const + +describe('getExplorerLink', () => { + it('returns address URL using chain block explorer', () => { + const url = getExplorerLink({ chain, hashOrAddress: address }) + expect(url).toBe(`${chain.blockExplorers?.default.url}/address/${address}`) + }) + + it('returns tx URL using chain block explorer for a hash', () => { + const url = getExplorerLink({ chain, hashOrAddress: txHash }) + expect(url).toBe(`${chain.blockExplorers?.default.url}/tx/${txHash}`) + }) + + it('uses custom explorerUrl for an address', () => { + const explorerUrl = 'https://custom.explorer.io' + const url = getExplorerLink({ chain, hashOrAddress: address, explorerUrl }) + expect(url).toBe(`${explorerUrl}/address/${address}`) + }) + + it('uses custom explorerUrl for a tx hash', () => { + const explorerUrl = 'https://custom.explorer.io' + const url = getExplorerLink({ chain, hashOrAddress: txHash, explorerUrl }) + expect(url).toBe(`${explorerUrl}/tx/${txHash}`) + }) + + it('throws for an invalid hash or address', () => { + expect(() => + // biome-ignore lint/suspicious/noExplicitAny: intentionally testing invalid input + getExplorerLink({ chain, hashOrAddress: 'not-valid' as any }), + ).toThrow('Invalid hash or address') + }) + + it('works with a chain that has no default block explorer (explorerUrl provided)', () => { + const chainWithoutExplorer: Chain = { ...chain, blockExplorers: undefined } + const explorerUrl = 'https://fallback.explorer.io' + const url = getExplorerLink({ + chain: chainWithoutExplorer, + hashOrAddress: address, + explorerUrl, + }) + expect(url).toBe(`${explorerUrl}/address/${address}`) + }) +}) diff --git a/src/utils/strings.test.ts b/src/utils/strings.test.ts new file mode 100644 index 00000000..e3e8d775 --- /dev/null +++ b/src/utils/strings.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest' +import { getTruncatedHash, truncateStringInTheMiddle } from './strings' + +describe('truncateStringInTheMiddle', () => { + it('truncates a long string keeping start and end', () => { + const result = truncateStringInTheMiddle('0x1234567890abcdef1234567890abcdef12345678', 8, 6) + expect(result).toBe('0x123456...345678') + }) + + it('returns the original string when it fits within start + end length', () => { + const result = truncateStringInTheMiddle('short', 4, 4) + expect(result).toBe('short') + }) + + it('returns the original string when length equals start + end exactly', () => { + const result = truncateStringInTheMiddle('1234567890', 5, 5) + expect(result).toBe('1234567890') + }) + + it('truncates when length exceeds start + end by one', () => { + const result = truncateStringInTheMiddle('12345678901', 5, 5) + expect(result).toBe('12345...78901') + }) + + it('handles empty string', () => { + const result = truncateStringInTheMiddle('', 4, 4) + expect(result).toBe('') + }) + + it('handles asymmetric start and end positions', () => { + const result = truncateStringInTheMiddle('abcdefghijklmnop', 3, 7) + expect(result).toBe('abc...jklmnop') + }) + + it('handles start position of 0', () => { + const result = truncateStringInTheMiddle('abcdefghij', 0, 3) + expect(result).toBe('...hij') + }) +}) + +describe('getTruncatedHash', () => { + const address = '0x1234567890abcdef1234567890abcdef12345678' + const txHash = '0xd85ef8c70dc31a4f8d5bf0331e1eac886935905f15d32e71b348df745cd38e19' + + it('truncates with default length of 6', () => { + const result = getTruncatedHash(address) + // 0x + 6 chars ... last 6 chars + expect(result).toBe('0x123456...345678') + }) + + it('truncates with custom length', () => { + const result = getTruncatedHash(address, 4) + // 0x + 4 chars ... last 4 chars + expect(result).toBe('0x1234...5678') + }) + + it('truncates a transaction hash', () => { + const result = getTruncatedHash(txHash, 6) + expect(result).toBe('0xd85ef8...d38e19') + }) + + it('clamps length to minimum of 1', () => { + const result = getTruncatedHash(address, 0) + // length clamped to 1: 0x + 1 char ... last 1 char + expect(result).toBe('0x1...8') + }) + + it('clamps length to maximum of 16', () => { + const result = getTruncatedHash(address, 100) + // address = '0x1234567890abcdef1234567890abcdef12345678' (42 chars) + // length clamped to 16: slice(0, 18) = '0x1234567890abcdef', slice(42-16, 42) = '90abcdef12345678' + expect(result).toBe('0x1234567890abcdef...90abcdef12345678') + }) + + it('handles negative length by clamping to 1', () => { + const result = getTruncatedHash(address, -5) + expect(result).toBe('0x1...8') + }) +}) From 7edb47f9b971224b3f07f408e665ea4313f97127 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:55:14 -0300 Subject: [PATCH 2/3] test: address review feedback on Tier 1 test suite - Fix env.test.ts test names that incorrectly claimed to test defaults while .env.test was setting those vars explicitly - Assert exact value for PUBLIC_WALLETCONNECT_PROJECT_ID (was typeof check) - Fix createMockWeb3Status shape to match actual Web3Status interface (was using isConnected/chainId/publicClient; hook uses isWalletConnected/ walletChainId/readOnlyClient) - Hardcode expected explorer URL in getExplorerLink assertions instead of deriving from mock chain (prevents false positives if mock misconfigured) --- src/env.test.ts | 11 ++++++----- src/test-utils.tsx | 17 +++++++++++------ src/utils/getExplorerLink.test.ts | 4 ++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/env.test.ts b/src/env.test.ts index 648db0f6..7abfc60f 100644 --- a/src/env.test.ts +++ b/src/env.test.ts @@ -11,8 +11,8 @@ describe('env', () => { expect(env.PUBLIC_APP_NAME).toBe('dAppBooster Test') }) - it('defaults PUBLIC_NATIVE_TOKEN_ADDRESS to zero address when not set', () => { - // .env.test sets it to the zero address explicitly + it('reads and normalizes PUBLIC_NATIVE_TOKEN_ADDRESS from env', () => { + // .env.test sets it to the zero address; the schema lowercases the value expect(env.PUBLIC_NATIVE_TOKEN_ADDRESS).toBe(zeroAddress.toLowerCase()) }) @@ -32,7 +32,8 @@ describe('env', () => { expect(env.PUBLIC_INCLUDE_TESTNETS).toBe(true) }) - it('defaults PUBLIC_SUBGRAPHS_ENVIRONMENT to production', () => { + it('reads PUBLIC_SUBGRAPHS_ENVIRONMENT from test env', () => { + // .env.test sets it to 'production'; to test the schema default use vi.resetModules() expect(env.PUBLIC_SUBGRAPHS_ENVIRONMENT).toBe('production') }) @@ -40,9 +41,9 @@ describe('env', () => { expect(env.PUBLIC_SUBGRAPHS_API_KEY).toBe('test-api-key') }) - it('exposes PUBLIC_WALLETCONNECT_PROJECT_ID with empty string default', () => { + it('exposes PUBLIC_WALLETCONNECT_PROJECT_ID from test env', () => { // .env.test sets it to 'test-project-id' - expect(typeof env.PUBLIC_WALLETCONNECT_PROJECT_ID).toBe('string') + expect(env.PUBLIC_WALLETCONNECT_PROJECT_ID).toBe('test-project-id') }) it('optional RPC vars are undefined when not set in test env', () => { diff --git a/src/test-utils.tsx b/src/test-utils.tsx index 124d95c3..d60fd802 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -22,16 +22,21 @@ export function createMockWeb3Status(overrides?: Partial {}, - switchChain: undefined, + switchChain: (_chainId?: number) => {}, } } diff --git a/src/utils/getExplorerLink.test.ts b/src/utils/getExplorerLink.test.ts index 31398572..c7ca4fad 100644 --- a/src/utils/getExplorerLink.test.ts +++ b/src/utils/getExplorerLink.test.ts @@ -12,12 +12,12 @@ const txHash = '0xd85ef8c70dc31a4f8d5bf0331e1eac886935905f15d32e71b348df745cd38e describe('getExplorerLink', () => { it('returns address URL using chain block explorer', () => { const url = getExplorerLink({ chain, hashOrAddress: address }) - expect(url).toBe(`${chain.blockExplorers?.default.url}/address/${address}`) + expect(url).toBe(`https://mock.explorer.url/address/${address}`) }) it('returns tx URL using chain block explorer for a hash', () => { const url = getExplorerLink({ chain, hashOrAddress: txHash }) - expect(url).toBe(`${chain.blockExplorers?.default.url}/tx/${txHash}`) + expect(url).toBe(`https://mock.explorer.url/tx/${txHash}`) }) it('uses custom explorerUrl for an address', () => { From 40e06d3e6aa2628f86a5ee20fe59fc5d52a00924 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:52:46 -0300 Subject: [PATCH 3/3] test: address review feedback on Tier 1 test suite (round 2) - Use @/src/env alias import in env.test.ts (matches repo convention) - Rename 'checksummed zero address' test to 'zero address string literal' (no checksum casing difference in this value; name was misleading) --- src/env.test.ts | 2 +- src/utils/address.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/env.test.ts b/src/env.test.ts index 7abfc60f..07c3a923 100644 --- a/src/env.test.ts +++ b/src/env.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest' // env.ts reads import.meta.env at module load time. // Vitest loads .env.test automatically for the "test" mode, // so PUBLIC_APP_NAME, PUBLIC_SUBGRAPHS_*, etc. are set via .env.test. -import { env } from './env' +import { env } from '@/src/env' describe('env', () => { it('exposes PUBLIC_APP_NAME from test env', () => { diff --git a/src/utils/address.test.ts b/src/utils/address.test.ts index 6a49fd05..754488d8 100644 --- a/src/utils/address.test.ts +++ b/src/utils/address.test.ts @@ -19,8 +19,8 @@ describe('isNativeToken', () => { expect(isNativeToken(zeroAddress.toLowerCase())).toBe(true) }) - it('returns true for a checksummed zero address', () => { - // zeroAddress is already lowercase, but ensure uppercase hex still matches + it('returns true for the zero address string literal', () => { + // zeroAddress is already lowercase; the literal string is identical — testing the exact value expect(isNativeToken('0x0000000000000000000000000000000000000000')).toBe(true) })