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)
})