Skip to content

Commit 93099cb

Browse files
Merge branch 'main' into feature/fet-2603-add-poap-support
2 parents 0723667 + 4b769d2 commit 93099cb

File tree

14 files changed

+927
-68
lines changed

14 files changed

+927
-68
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+
})

0 commit comments

Comments
 (0)