Skip to content

Commit 00d4441

Browse files
Merge branch 'main' into test-improvements
2 parents 9ba16c7 + 4b769d2 commit 00d4441

File tree

11 files changed

+228
-19
lines changed

11 files changed

+228
-19
lines changed

.github/actions/setup/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ runs:
66
- name: Install pnpm
77
uses: pnpm/action-setup@v4
88
with:
9-
version: 9.3.0
9+
version: 10.23.0
1010

1111
- name: Install Node.js
1212
uses: actions/setup-node@v4

.github/workflows/test-wallet.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ jobs:
1616
timeout-minutes: 20
1717
runs-on: ubuntu-latest
1818
container:
19-
image: mcr.microsoft.com/playwright:v1.55.0-jammy
19+
image: mcr.microsoft.com/playwright:v1.56.1-jammy
2020
steps:
2121
- uses: actions/checkout@v4
2222

2323
- name: Setup pnpm
2424
uses: pnpm/action-setup@v4
2525
with:
26-
version: 9.3.0
26+
version: 10.23.0
2727

2828
- name: Setup Node.js
2929
uses: actions/setup-node@v4
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { expect } from '@playwright/test'
2+
import type { Page } from '@playwright/test'
3+
4+
import { setPrimaryName } from '@ensdomains/ensjs/wallet'
5+
6+
import { test } from '../../../playwright'
7+
import { createAccounts } from '../../../playwright/fixtures/accounts'
8+
import {
9+
waitForTransaction,
10+
walletClient,
11+
} from '../../../playwright/fixtures/contracts/utils/addTestContracts'
12+
import type { Login } from '../../../playwright/fixtures/login'
13+
14+
// Constants
15+
const INVALID_RESOLVER = '0x0000000000000000000000000000000000000001'
16+
const UNIVERSAL_RESOLVER_RESOLVE_SELECTOR = '0x82ad56cb' // resolve(bytes,bytes)
17+
const NAMES_LIST_TIMEOUT = 15000
18+
const RETRY_TIMEOUT = 45000
19+
20+
// Helper to get user address consistently
21+
const getUserAddress = () => createAccounts().getAddress('user') as `0x${string}`
22+
23+
// Helper to set primary name and wait for transaction
24+
async function setPrimaryNameAndWait(name: string, userAddress: `0x${string}`) {
25+
const tx = await setPrimaryName(walletClient, {
26+
name,
27+
account: userAddress,
28+
})
29+
await waitForTransaction(tx)
30+
}
31+
32+
// Helper to create a name owned by user
33+
async function createUserName(makeName: any, label: string) {
34+
return makeName({
35+
label,
36+
type: 'legacy',
37+
owner: 'user',
38+
manager: 'user',
39+
addr: 'user',
40+
})
41+
}
42+
43+
// Helper to set an invalid resolver that triggers ContractFunctionExecutionError
44+
async function setInvalidResolver(
45+
page: Page,
46+
name: string,
47+
login: InstanceType<typeof Login>,
48+
makePageObject: any,
49+
) {
50+
const morePage = makePageObject('MorePage')
51+
const transactionModal = makePageObject('TransactionModal')
52+
53+
await morePage.goto(name)
54+
await login.connect()
55+
56+
await morePage.editResolverButton.click()
57+
await page.getByTestId('custom-resolver-radio').check()
58+
await page.getByTestId('dogfood').fill(INVALID_RESOLVER)
59+
await page.getByTestId('update-button').click()
60+
await transactionModal.autoComplete()
61+
}
62+
63+
// Helper to assert address page loaded gracefully (doesn't crash)
64+
async function assertAddressPageLoaded(page: Page) {
65+
await page.waitForLoadState('networkidle')
66+
67+
const hasProfile = await page
68+
.getByTestId('profile-snippet')
69+
.isVisible()
70+
.catch(() => false)
71+
const hasNoProfile = await page
72+
.getByTestId('no-profile-snippet')
73+
.isVisible()
74+
.catch(() => false)
75+
76+
expect(hasProfile || hasNoProfile).toBeTruthy()
77+
}
78+
79+
// Helper to intercept and fail RPC calls to test retry logic
80+
async function setupRPCInterception(page: Page, maxFailures: number): Promise<() => number> {
81+
let requestCount = 0
82+
83+
await page.route('**/*', async (route) => {
84+
const request = route.request()
85+
const url = request.url()
86+
87+
if (url.includes(':8545') || url.includes('localhost:8545')) {
88+
try {
89+
const postData = request.postDataJSON()
90+
91+
if (postData?.method === 'eth_call') {
92+
const data = postData?.params?.[0]?.data
93+
94+
if (data && data.startsWith(UNIVERSAL_RESOLVER_RESOLVE_SELECTOR)) {
95+
requestCount += 1
96+
97+
if (requestCount <= maxFailures) {
98+
// Simulate network error (not ContractFunctionExecutionError)
99+
await route.abort('failed')
100+
return
101+
}
102+
}
103+
}
104+
} catch (e) {
105+
// If we can't parse the request, continue normally
106+
}
107+
}
108+
109+
await route.continue()
110+
})
111+
112+
return () => requestCount
113+
}
114+
115+
test.describe('Address page error handling', () => {
116+
// Tests for usePrimaryName.ts fix that catches ContractFunctionExecutionError
117+
// and returns null (graceful failure) while still throwing other errors for retry
118+
119+
test('should load address page without crashing when getName throws ContractFunctionExecutionError', async ({
120+
page,
121+
login,
122+
makeName,
123+
makePageObject,
124+
}) => {
125+
test.slow()
126+
127+
// SETUP: Create name with invalid resolver + set as primary name
128+
const name = await createUserName(makeName, 'error-test-name')
129+
await setInvalidResolver(page, name, login, makePageObject)
130+
131+
const profilePage = makePageObject('ProfilePage')
132+
const transactionModal = makePageObject('TransactionModal')
133+
await profilePage.goto(name)
134+
await page.getByText('Set as primary name').click()
135+
await transactionModal.autoComplete()
136+
137+
// ACTION: Navigate to address page (triggers getName with invalid resolver)
138+
const userAddress = getUserAddress()
139+
await page.goto(`/${userAddress}`)
140+
141+
// ASSERT: Page loads gracefully without crashing
142+
await assertAddressPageLoaded(page)
143+
144+
// ASSERT: Other functionality still works (names list loads)
145+
await expect(page.getByTestId('names-list')).toBeVisible({ timeout: NAMES_LIST_TIMEOUT })
146+
})
147+
148+
test('should retry on non-ContractFunctionExecutionError and not return null early', async ({
149+
page,
150+
login,
151+
makeName,
152+
}) => {
153+
test.slow()
154+
155+
// SETUP: Create valid name and set as primary name
156+
const name = await createUserName(makeName, 'retry-test-name')
157+
const userAddress = getUserAddress()
158+
await setPrimaryNameAndWait(name, userAddress)
159+
160+
// SETUP: Intercept RPC calls to simulate network failures
161+
const maxFailures = 2
162+
const getRequestCount = await setupRPCInterception(page, maxFailures)
163+
164+
// ACTION: Navigate to address page (triggers getName with simulated failures)
165+
await page.goto(`/${userAddress}`)
166+
await login.connect()
167+
168+
// ASSERT: Page eventually loads after retries
169+
await page.waitForLoadState('networkidle')
170+
await expect(page.getByTestId('profile-snippet')).toBeVisible({ timeout: RETRY_TIMEOUT })
171+
await expect(page.getByTestId('profile-snippet-name')).toContainText(name)
172+
173+
// ASSERT: Retries occurred (proves non-ContractFunctionExecutionError thrown, not caught)
174+
const requestCount = getRequestCount()
175+
expect(requestCount).toBeGreaterThan(maxFailures)
176+
expect(requestCount).toBeGreaterThanOrEqual(3) // At least 2 failures + 1 success
177+
})
178+
179+
test.afterEach(async () => {
180+
// Clean up: reset primary name after each test
181+
await setPrimaryName(walletClient, {
182+
name: '',
183+
account: getUserAddress(),
184+
}).catch(() => {
185+
// Ignore errors during cleanup
186+
})
187+
})
188+
})

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@
174174
"next-dev-https": "^0.1.2",
175175
"next-router-mock": "^0.9.10",
176176
"pako": "^2.1.0",
177-
"playwright-core": "^1.55.0",
177+
"playwright-core": "^1.56.1",
178178
"postcss-styled-syntax": "^0.7.1",
179179
"prettier": "3.0.3",
180180
"sitemap": "^7.1.1",
@@ -302,5 +302,5 @@
302302
"@ensdomains/dnsprovejs@0.5.1": "patches/@ensdomains__dnsprovejs@0.5.1.patch"
303303
}
304304
},
305-
"packageManager": "pnpm@9.3.0"
305+
"packageManager": "pnpm@10.23.0"
306306
}

pnpm-lock.yaml

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/locales/en/register.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
"base": "0xb8c2C2...",
9797
"matic": "0xb8c2C2...",
9898
"linea": "0xb8c2C2...",
99-
"scr": "0xb8c2C2..."
99+
"scr": "0xb8c2C2...",
100+
"celo": "0xb8c2C2..."
100101
}
101102
},
102103
"website": {

src/assets/address/CeloIcon.svg

Lines changed: 13 additions & 1 deletion
Loading
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"eth":{"id":1,"name":"Ethereum","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Etherscan","url":"https://etherscan.io","apiUrl":"https://api.etherscan.io/api"}}},"btc":{"name":"Bitcoin","nativeCurrency":{"name":"Bitcoin","symbol":"BTC","decimals":8},"blockExplorers":{"default":{"name":"Blockchair","url":"https://blockchair.com/bitcoin"}}},"sol":{"name":"Solana","nativeCurrency":{"name":"Solana","symbol":"SOL","decimals":9},"blockExplorers":{"default":{"name":"Solana","url":"https://explorer.solana.com/"}}},"op":{"id":10,"name":"OP Mainnet","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Optimism Explorer","url":"https://optimistic.etherscan.io","apiUrl":"https://api-optimistic.etherscan.io/api"}}},"arb1":{"id":42161,"name":"Arbitrum One","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Arbiscan","url":"https://arbiscan.io","apiUrl":"https://api.arbiscan.io/api"}}},"base":{"id":8453,"name":"Base","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Basescan","url":"https://basescan.org","apiUrl":"https://api.basescan.org/api"}}},"matic":{"id":137,"name":"Polygon","nativeCurrency":{"name":"MATIC","symbol":"MATIC","decimals":18},"blockExplorers":{"default":{"name":"PolygonScan","url":"https://polygonscan.com","apiUrl":"https://api.polygonscan.com/api"}}},"linea":{"id":59144,"name":"Linea Mainnet","nativeCurrency":{"name":"Linea Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Etherscan","url":"https://lineascan.build","apiUrl":"https://api.lineascan.build/api"}}},"scr":{"id":534352,"name":"Scroll Mainnet","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Scrollscan","url":"https://scrollscan.com","apiUrl":"https://api.scrollscan.com/api"}}}}
1+
{"eth":{"id":1,"name":"Ethereum","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Etherscan","url":"https://etherscan.io","apiUrl":"https://api.etherscan.io/api"}}},"btc":{"name":"Bitcoin","nativeCurrency":{"name":"Bitcoin","symbol":"BTC","decimals":8},"blockExplorers":{"default":{"name":"Blockchair","url":"https://blockchair.com/bitcoin"}}},"sol":{"name":"Solana","nativeCurrency":{"name":"Solana","symbol":"SOL","decimals":9},"blockExplorers":{"default":{"name":"Solana","url":"https://explorer.solana.com/"}}},"op":{"id":10,"name":"OP Mainnet","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Optimism Explorer","url":"https://optimistic.etherscan.io","apiUrl":"https://api-optimistic.etherscan.io/api"}}},"arb1":{"id":42161,"name":"Arbitrum One","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Arbiscan","url":"https://arbiscan.io","apiUrl":"https://api.arbiscan.io/api"}}},"base":{"id":8453,"name":"Base","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Basescan","url":"https://basescan.org","apiUrl":"https://api.basescan.org/api"}}},"matic":{"id":137,"name":"Polygon","nativeCurrency":{"name":"MATIC","symbol":"MATIC","decimals":18},"blockExplorers":{"default":{"name":"PolygonScan","url":"https://polygonscan.com","apiUrl":"https://api.polygonscan.com/api"}}},"linea":{"id":59144,"name":"Linea Mainnet","nativeCurrency":{"name":"Linea Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Etherscan","url":"https://lineascan.build","apiUrl":"https://api.lineascan.build/api"}}},"scr":{"id":534352,"name":"Scroll Mainnet","nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorers":{"default":{"name":"Scrollscan","url":"https://scrollscan.com","apiUrl":"https://api.scrollscan.com/api"}}},"celo":{"id":42220,"name":"Celo","nativeCurrency":{"name":"Celo","symbol":"CELO","decimals":18},"blockExplorers":{"default":{"name":"Celoscan","url":"https://celoscan.io","apiUrl":"https://api.celoscan.io/api"}}}}

src/constants/coinsWithIcons.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"default",
33
"eth",
44
"btc",
5+
"celo",
56
"bnb",
67
"xrp",
78
"matic",
@@ -24,7 +25,6 @@
2425
"bcn",
2526
"btg",
2627
"bts",
27-
"celo",
2828
"ckb",
2929
"clo",
3030
"cro",

src/constants/supportedAddresses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const supportedAddresses = [
99
'matic',
1010
'linea',
1111
'scr',
12+
'celo',
1213
] as const
1314

1415
export type SupportedAddress = (typeof supportedAddresses)[number]

0 commit comments

Comments
 (0)