|
| 1 | +import { BrowserContext, expect, Page, test } from '@playwright/test' |
| 2 | +import dappwright from '@tenkeylabs/dappwright' |
| 3 | +import type { Dappwright } from '@tenkeylabs/dappwright' |
| 4 | + |
| 5 | +import { dateToDateInput, roundDurationWithDay, secondsToDateInput } from '@app/utils/date' |
| 6 | + |
| 7 | +import { SafeEnsConfig } from './config/safe-ens-config' |
| 8 | + |
| 9 | +// Global variables to share state |
| 10 | +let metaMask: Dappwright |
| 11 | +let page: Page |
| 12 | +let context: BrowserContext |
| 13 | + |
| 14 | +// Connect wallet to ENS app Sepolia |
| 15 | +async function connectWalletToEns(): Promise<void> { |
| 16 | + console.log('🔗 Connecting MetaMask to Sepolia ENS...') |
| 17 | + await page.goto('https://sepolia.app.ens.domains') |
| 18 | + await page.waitForTimeout(3000) |
| 19 | + |
| 20 | + // Wait for "Connect Wallet" button and click |
| 21 | + const connectButton = page |
| 22 | + .locator( |
| 23 | + 'button:has-text("Connect"), button:has-text("Connect Wallet"), [data-testid="connect-button"]', |
| 24 | + ) |
| 25 | + .first() |
| 26 | + await connectButton.waitFor({ timeout: 15000 }) |
| 27 | + await connectButton.click() |
| 28 | + console.log('🔘 Connect Wallet button clicked') |
| 29 | + await page.waitForTimeout(1000) |
| 30 | + |
| 31 | + // Wait for wallet modal |
| 32 | + const modal = page.locator('[role="dialog"], .wallet-modal') |
| 33 | + await modal.waitFor({ timeout: 15000 }) |
| 34 | + console.log('💬 Wallet modal detected') |
| 35 | + |
| 36 | + // Wait for MetaMask option inside modal |
| 37 | + const metamaskOption = modal.locator('button', { hasText: 'MetaMask' }).first() |
| 38 | + await metamaskOption.waitFor({ timeout: 15000 }) |
| 39 | + await metamaskOption.click() |
| 40 | + console.log('🦊 MetaMask option clicked, waiting for extension popup...') |
| 41 | + |
| 42 | + // Poll for MetaMask notification popup |
| 43 | + let mmPage |
| 44 | + let attempts = 0 |
| 45 | + |
| 46 | + while (attempts < 20 && !mmPage) { |
| 47 | + mmPage = context |
| 48 | + .pages() |
| 49 | + .find((p) => p.url().includes('chrome-extension://') && p.url().includes('notification.html')) |
| 50 | + |
| 51 | + if (mmPage) break |
| 52 | + // eslint-disable-next-line no-await-in-loop |
| 53 | + await page.waitForTimeout(500) |
| 54 | + |
| 55 | + attempts += 1 |
| 56 | + } |
| 57 | + |
| 58 | + if (!mmPage) { |
| 59 | + throw new Error('MetaMask popup not found') |
| 60 | + } |
| 61 | + |
| 62 | + await mmPage.bringToFront() |
| 63 | + |
| 64 | + // Optional: select first account if visible |
| 65 | + const accountButton = mmPage.locator('div.account-list button').first() |
| 66 | + if (await accountButton.isVisible({ timeout: 5000 })) { |
| 67 | + await accountButton.click() |
| 68 | + const nextButton = mmPage.locator('button:has-text("Next")').first() |
| 69 | + if (await nextButton.isVisible({ timeout: 3000 })) { |
| 70 | + await nextButton.click() |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + // Confirm connection |
| 75 | + const confirmButton = mmPage |
| 76 | + .locator('button:has-text("Connect"), button:has-text("Confirm"), .btn-primary') |
| 77 | + .first() |
| 78 | + await confirmButton.waitFor({ timeout: 5000 }) |
| 79 | + await confirmButton.click() |
| 80 | + console.log('✅ MetaMask connection confirmed') |
| 81 | + |
| 82 | + // Bring main page to front and wait a few seconds |
| 83 | + await page.bringToFront() |
| 84 | + await page.waitForTimeout(3000) |
| 85 | + |
| 86 | + // Optional: verify connection |
| 87 | + const stillVisible = await page |
| 88 | + .locator('button:has-text("Connect"), [data-testid="connect-button"]') |
| 89 | + .isVisible() |
| 90 | + if (stillVisible) { |
| 91 | + console.log('⚠️ Wallet may not have connected — check MetaMask popup manually') |
| 92 | + } else { |
| 93 | + console.log('✅ Wallet successfully connected on ENS site') |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +// Confirm transaction helper |
| 98 | +async function confirmTransactionWithMetaMask(): Promise<void> { |
| 99 | + console.log(`🦊 Waiting for MetaMask popup...`) |
| 100 | + |
| 101 | + // Listen for a new popup page to open |
| 102 | + const [mmPage] = await Promise.all([ |
| 103 | + page.context().waitForEvent('page', { timeout: 15000 }), // wait up to 15s |
| 104 | + // Ensure we click or trigger the action that opens the popup BEFORE this function is called |
| 105 | + ]) |
| 106 | + |
| 107 | + // Verify this is actually a MetaMask notification page |
| 108 | + if ( |
| 109 | + !mmPage.url().includes('chrome-extension://') || |
| 110 | + !mmPage.url().includes('notification.html') |
| 111 | + ) { |
| 112 | + throw new Error(`Unexpected popup detected: ${mmPage.url()}`) |
| 113 | + } |
| 114 | + |
| 115 | + await mmPage.bringToFront() |
| 116 | + |
| 117 | + // Wait for confirm button to appear and click it |
| 118 | + const confirmButton = mmPage.locator('button:has-text("Confirm")') |
| 119 | + await confirmButton.waitFor({ timeout: 10000 }) |
| 120 | + await confirmButton.click() |
| 121 | + |
| 122 | + console.log(`✅ MetaMask transaction confirmed`) |
| 123 | + |
| 124 | + await page.bringToFront() |
| 125 | +} |
| 126 | + |
| 127 | +// Extend owned name |
| 128 | +async function extendOwnedNameOnSepoliaApp(): Promise<void> { |
| 129 | + const name = 'extend-name-test.eth' |
| 130 | + |
| 131 | + console.log(`🎯 Starting extension for ${name}`) |
| 132 | + |
| 133 | + // Search for name |
| 134 | + const searchInput = page.locator('input[placeholder="Search for a name"]') |
| 135 | + await searchInput.waitFor({ timeout: 15000 }) |
| 136 | + await searchInput.fill(name) |
| 137 | + await searchInput.press('Enter') |
| 138 | + |
| 139 | + // Grab the current expiry timestamp from profile |
| 140 | + const expiryElement = page.getByTestId('owner-profile-button-name.expiry') |
| 141 | + await expiryElement.waitFor({ state: 'visible', timeout: 15000 }) |
| 142 | + |
| 143 | + const timestampAttr = await expiryElement.getAttribute('data-timestamp') |
| 144 | + if (!timestampAttr) throw new Error('❌ Could not read expiry timestamp from UI') |
| 145 | + |
| 146 | + const currentExpiryTimestamp = parseInt(timestampAttr, 10) |
| 147 | + console.log(`📅 Current expiry: ${new Date(currentExpiryTimestamp).toISOString()}`) |
| 148 | + |
| 149 | + // Click "extend" |
| 150 | + const extendButton = page.getByTestId('extend-button') |
| 151 | + await extendButton.waitFor({ state: 'visible', timeout: 15000 }) |
| 152 | + await extendButton.click() |
| 153 | + |
| 154 | + // Switch to "pick by date" |
| 155 | + const dateSelection = page.getByTestId('date-selection') |
| 156 | + await expect(dateSelection).toHaveText('Pick by date') |
| 157 | + await dateSelection.click() |
| 158 | + |
| 159 | + // Fill the calendar with a date one day later |
| 160 | + const expiryTime = currentExpiryTimestamp / 1000 |
| 161 | + const calendar = page.getByTestId('calendar') |
| 162 | + const dayLater = await page.evaluate((ts) => { |
| 163 | + const expiryDate = new Date(ts) |
| 164 | + expiryDate.setDate(expiryDate.getDate() + 1) |
| 165 | + return expiryDate |
| 166 | + }, currentExpiryTimestamp) |
| 167 | + |
| 168 | + await calendar.fill(dateToDateInput(dayLater)) |
| 169 | + await expect(page.getByTestId('calendar-date')).toHaveValue( |
| 170 | + secondsToDateInput(expiryTime + roundDurationWithDay(dayLater, expiryTime)), |
| 171 | + ) |
| 172 | + |
| 173 | + // Confirm extension |
| 174 | + const confirmButton = page.getByTestId('extend-names-confirm') |
| 175 | + await confirmButton.click() |
| 176 | + |
| 177 | + // Transaction modal check BEFORE confirming |
| 178 | + await expect(page.getByText('1 day')).toBeVisible() |
| 179 | + await page.locator('text=Open Wallet').waitFor({ timeout: 10000 }) |
| 180 | + await page.locator('text=Open Wallet').click() |
| 181 | + |
| 182 | + // Confirm in Metamask pop up |
| 183 | + await confirmTransactionWithMetaMask() |
| 184 | + |
| 185 | + // Wait for transaction to complete |
| 186 | + await page.waitForTimeout(25000) |
| 187 | + |
| 188 | + // Close transaction complete modal |
| 189 | + const transactionCompleteButton = page.getByTestId('transaction-modal-complete-button') |
| 190 | + await transactionCompleteButton.click() |
| 191 | + |
| 192 | + // Verify new expiry is +1 day |
| 193 | + const newTimestampAttr = await expiryElement.getAttribute('data-timestamp') |
| 194 | + if (!newTimestampAttr) throw new Error('❌ Could not read new expiry timestamp') |
| 195 | + |
| 196 | + const newExpiryTimestamp = parseInt(newTimestampAttr, 10) |
| 197 | + const expectedDate = new Date(currentExpiryTimestamp) |
| 198 | + expectedDate.setDate(expectedDate.getDate() + 1) |
| 199 | + const expectedTimestamp = expectedDate.getTime() |
| 200 | + |
| 201 | + expect(newExpiryTimestamp).toBe(expectedTimestamp) |
| 202 | + console.log( |
| 203 | + `✅ Name successfully extended by 1 day (new expiry: ${new Date( |
| 204 | + newExpiryTimestamp, |
| 205 | + ).toISOString()})`, |
| 206 | + ) |
| 207 | +} |
| 208 | + |
| 209 | +// Extend unowned name |
| 210 | +async function extendUnownedNameSepolia(): Promise<void> { |
| 211 | + const name = 'user1-extend.eth' |
| 212 | + |
| 213 | + console.log(`🎯 Starting extension for ${name}`) |
| 214 | + |
| 215 | + // Search for name |
| 216 | + const searchInput = page.locator('input[placeholder="Search for a name"]') |
| 217 | + await searchInput.waitFor({ timeout: 15000 }) |
| 218 | + await searchInput.fill(name) |
| 219 | + await searchInput.press('Enter') |
| 220 | + |
| 221 | + // Grab the current expiry timestamp from profile |
| 222 | + const expiryElement = page.getByTestId('owner-profile-button-name.expiry') |
| 223 | + await expiryElement.waitFor({ state: 'visible', timeout: 15000 }) |
| 224 | + |
| 225 | + const timestampAttr = await expiryElement.getAttribute('data-timestamp') |
| 226 | + if (!timestampAttr) throw new Error('❌ Could not read expiry timestamp from UI') |
| 227 | + |
| 228 | + const currentExpiryTimestamp = parseInt(timestampAttr, 10) |
| 229 | + console.log(`📅 Current expiry: ${new Date(currentExpiryTimestamp).toISOString()}`) |
| 230 | + |
| 231 | + // Click "extend" |
| 232 | + const extendButton = page.getByTestId('extend-button') |
| 233 | + await extendButton.waitFor({ state: 'visible', timeout: 15000 }) |
| 234 | + await extendButton.click() |
| 235 | + |
| 236 | + // Acknowledge extension of unowned name |
| 237 | + const extendUnownedConfirm = page.getByTestId('extend-names-confirm') |
| 238 | + await extendUnownedConfirm.waitFor({ state: 'visible', timeout: 5000 }) |
| 239 | + await extendUnownedConfirm.click() |
| 240 | + |
| 241 | + // Switch to "pick by date" |
| 242 | + const dateSelection = page.getByTestId('date-selection') |
| 243 | + await expect(dateSelection).toHaveText('Pick by date', { timeout: 5000 }) |
| 244 | + await dateSelection.click() |
| 245 | + |
| 246 | + // Fill the calendar with a date one day later |
| 247 | + const expiryTime = currentExpiryTimestamp / 1000 |
| 248 | + const calendar = page.getByTestId('calendar') |
| 249 | + const dayLater = await page.evaluate((ts) => { |
| 250 | + const expiryDate = new Date(ts) |
| 251 | + expiryDate.setDate(expiryDate.getDate() + 1) |
| 252 | + return expiryDate |
| 253 | + }, currentExpiryTimestamp) |
| 254 | + |
| 255 | + await calendar.fill(dateToDateInput(dayLater)) |
| 256 | + await expect(page.getByTestId('calendar-date')).toHaveValue( |
| 257 | + secondsToDateInput(expiryTime + roundDurationWithDay(dayLater, expiryTime)), |
| 258 | + ) |
| 259 | + |
| 260 | + // Confirm extension |
| 261 | + const confirmButton = page.getByTestId('extend-names-confirm') |
| 262 | + await confirmButton.click() |
| 263 | + |
| 264 | + // Transaction modal check BEFORE confirming |
| 265 | + await expect(page.getByText('1 day')).toBeVisible() |
| 266 | + await page.locator('text=Open Wallet').waitFor({ timeout: 10000 }) |
| 267 | + await page.locator('text=Open Wallet').click() |
| 268 | + |
| 269 | + // Confirm in Metamask pop up |
| 270 | + await confirmTransactionWithMetaMask() |
| 271 | + |
| 272 | + // Wait for transaction to complete |
| 273 | + await page.waitForTimeout(15000) |
| 274 | + |
| 275 | + // Close transaction complete modal |
| 276 | + const transactionCompleteButton = page.getByTestId('transaction-modal-complete-button') |
| 277 | + await transactionCompleteButton.click() |
| 278 | + |
| 279 | + // Verify new expiry is +1 day |
| 280 | + const newTimestampAttr = await expiryElement.getAttribute('data-timestamp') |
| 281 | + if (!newTimestampAttr) throw new Error('❌ Could not read new expiry timestamp') |
| 282 | + |
| 283 | + const newExpiryTimestamp = parseInt(newTimestampAttr, 10) |
| 284 | + const expectedDate = new Date(currentExpiryTimestamp) |
| 285 | + expectedDate.setDate(expectedDate.getDate() + 1) |
| 286 | + const expectedTimestamp = expectedDate.getTime() |
| 287 | + |
| 288 | + expect(newExpiryTimestamp).toBe(expectedTimestamp) |
| 289 | + console.log( |
| 290 | + `✅ Name successfully extended by 1 day (new expiry: ${new Date( |
| 291 | + newExpiryTimestamp, |
| 292 | + ).toISOString()})`, |
| 293 | + ) |
| 294 | +} |
| 295 | + |
| 296 | +test.describe('ENS Sepolia Extend Name', () => { |
| 297 | + test.beforeAll('Setup Metamask', async () => { |
| 298 | + console.log('🦊 Setting up MetaMask...') |
| 299 | + const [mm, pg, ctx] = await dappwright.bootstrap('chromium', { |
| 300 | + wallet: 'metamask', |
| 301 | + version: SafeEnsConfig.METAMASK.VERSION, |
| 302 | + seed: SafeEnsConfig.SEED_PHRASE, |
| 303 | + password: SafeEnsConfig.WALLET_PASSWORD, |
| 304 | + headless: SafeEnsConfig.BROWSER.HEADLESS, |
| 305 | + slowMo: SafeEnsConfig.BROWSER.SLOW_MO, |
| 306 | + }) |
| 307 | + |
| 308 | + metaMask = mm |
| 309 | + page = pg |
| 310 | + context = ctx |
| 311 | + |
| 312 | + console.log('✅ MetaMask setup complete') |
| 313 | + |
| 314 | + // Switch to User 2 account |
| 315 | + await page.click('[data-testid="account-menu-icon"]') |
| 316 | + await page.click('[data-testid="multichain-account-menu-popover-action-button"]') |
| 317 | + await page.click('[data-testid="multichain-account-menu-popover-add-account"]') |
| 318 | + await page.click('[data-testid="submit-add-account-with-name"]') |
| 319 | + |
| 320 | + console.log('✅ Switched to User 2 account') |
| 321 | + |
| 322 | + try { |
| 323 | + await metaMask.switchNetwork('Sepolia') |
| 324 | + console.log('✅ Switched to Sepolia network') |
| 325 | + } catch (error) { |
| 326 | + console.log('⚠️ Could not switch to Sepolia:', error) |
| 327 | + } |
| 328 | + |
| 329 | + // Connect wallet to ENS Sepolia |
| 330 | + await connectWalletToEns() |
| 331 | + }) |
| 332 | + |
| 333 | + test('Connect MetaMask to ENS Sepolia', async () => { |
| 334 | + await expect( |
| 335 | + page.locator('button:has-text("Connect"), [data-testid="connect-button"]'), |
| 336 | + ).toBeHidden({ timeout: 5000 }) |
| 337 | + |
| 338 | + console.log('✅ Wallet is connected and ready') |
| 339 | + }) |
| 340 | + |
| 341 | + test('Extend user owned ENS name on Sepolia', async () => { |
| 342 | + await extendOwnedNameOnSepoliaApp() |
| 343 | + }) |
| 344 | + |
| 345 | + test('Extend not user owned ENS name on Sepolia', async () => { |
| 346 | + await extendUnownedNameSepolia() |
| 347 | + }) |
| 348 | +}) |
0 commit comments