Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 76 additions & 27 deletions core/wallet-test-utils/src/wallet-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { expect, Locator, Page, test } from '@playwright/test'
import { openWalletPicker } from './wallet-picker.js'
import {
openWalletPicker,
WALLET_PICKER_MODAL_HOST,
walletPickerModalRowByTitle,
type WalletPickerSurface,
} from './wallet-picker.js'

export interface NetworkFormInput {
id: string
Expand Down Expand Up @@ -121,14 +126,20 @@ export class WalletGateway {
'the dApp should offer a way to connect a wallet'
).toBeVisible()

const pickerPopup = await openWalletPicker(
dapp.dappPage,
connectButton
)
const picker = await openWalletPicker(dapp.dappPage, connectButton)

// Modal selection opens the WG login popup afterwards. Arm the
// waiter before click so we do not miss the popup event (CI race).
const loginPopupPromise =
picker.kind === 'modal'
? dapp.dappPage.waitForEvent('popup', { timeout: 30_000 })
: null

await this.selectFromWalletPicker(pickerPopup, args.customURL)
await this.selectFromWalletPicker(picker, args.customURL)

const popup = await this.waitForConnectFormPopup(pickerPopup)
const popup = await this.waitForConnectFormPopup(
picker.kind === 'popup' ? picker.page : await loginPopupPromise!
)
const selectNetwork = popup.getByLabel('Select a network')
await expect(
selectNetwork,
Expand Down Expand Up @@ -487,23 +498,46 @@ export class WalletGateway {
}

private async selectFromWalletPicker(
popup: Page,
picker: WalletPickerSurface,
customURL?: string
): Promise<void> {
const { page, kind } = picker

if (kind === 'modal') {
if (customURL !== undefined) {
await walletPickerModalRowByTitle(page, 'Remote Wallet').click()
const host = page.locator(WALLET_PICKER_MODAL_HOST)
const urlInput = host.getByLabel('Remote Wallet URL')
await urlInput.waitFor({ state: 'visible', timeout: 5_000 })
await urlInput.fill(customURL)
await host.locator('.gateway-connect-button').click()
return
}

const firstWallet = page
.locator(WALLET_PICKER_MODAL_HOST)
.getByRole('button')
.filter({ hasNotText: /^Remote Wallet/ })
.first()
await firstWallet.waitFor({ state: 'visible', timeout: 5_000 })
await firstWallet.click()
return
}

if (customURL !== undefined) {
const customUrlInput = popup.locator('.custom-url-input')
const customUrlInput = page.locator('.custom-url-input')
await customUrlInput.waitFor({ state: 'visible', timeout: 3000 })
await customUrlInput.fill(customURL)
await popup.locator('.btn-add').click()
await page.locator('.btn-add').click()
return
}

const walletCard = popup.locator('.wallet-card').first()
const walletCard = page.locator('.wallet-card').first()
await walletCard.waitFor({ state: 'visible', timeout: 3000 })
await walletCard.click()
}

private async waitForConnectFormPopup(initialPopup: Page): Promise<Page> {
private async waitForConnectFormPopup(initialPopup?: Page): Promise<Page> {
const hasConnectForm = async (page: Page): Promise<boolean> => {
try {
await page
Expand All @@ -523,27 +557,42 @@ export class WalletGateway {
}
}

// Pre-fix behavior: picker page itself transitions to connect form.
if (await hasConnectForm(initialPopup)) {
return initialPopup
if (initialPopup && !initialPopup.isClosed()) {
if (await hasConnectForm(initialPopup)) {
return initialPopup
}
// Popup may still be loading the connect form.
for (let i = 0; i < 40; i++) {
if (initialPopup.isClosed()) break
if (await hasConnectForm(initialPopup)) {
return initialPopup
}
await new Promise((resolve) => setTimeout(resolve, 250))
}
}

// New behavior: picker may close and the wallet connect form appears in
// a fresh popup. Poll the tracked popup first to avoid races.
for (let i = 0; i < 20; i++) {
const popup = this._popup
if (popup && !popup.isClosed() && (await hasConnectForm(popup))) {
return popup
for (let i = 0; i < 40; i++) {
const tracked = this._popup
if (
tracked &&
!tracked.isClosed() &&
(await hasConnectForm(tracked))
) {
return tracked
}

// Scan all context pages — popup may have opened before we waited.
const pages = this.requireDapp().dappPage.context().pages()
for (const page of pages) {
if (page.isClosed()) continue
if (await hasConnectForm(page)) {
return page
}
}

await new Promise((resolve) => setTimeout(resolve, 250))
}

const popup = await this.requireDapp().dappPage.waitForEvent('popup', {
timeout: 5000,
})
if (await hasConnectForm(popup)) {
return popup
}
throw new Error('wallet connect form popup did not appear')
}

Expand Down
61 changes: 58 additions & 3 deletions core/wallet-test-utils/src/wallet-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,66 @@

import type { Locator, Page } from '@playwright/test'

export const WALLET_PICKER_MODAL_HOST = '[data-swk-wallet-picker-modal]'

export type WalletPickerSurface = {
kind: 'modal' | 'popup'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really want to support both modal and popup style pickers? IMO I don't really see a reason to continue to provide the popup as an option, I'd be in favor of going full modal-only

cc @mjuchli-da @joel-da for thoughts

page: Page
dappPage: Page
}

export async function openWalletPicker(
dappPage: Page,
connectButton: Locator
): Promise<Page> {
const pickerPopup = dappPage.waitForEvent('popup')
): Promise<WalletPickerSurface> {
const modalHost = dappPage.locator(WALLET_PICKER_MODAL_HOST)

const popupRace = dappPage
.waitForEvent('popup', { timeout: 15_000 })
.then((page): WalletPickerSurface => ({
kind: 'popup',
page,
dappPage,
}))
.catch(() => null)

const modalRace = modalHost
.waitFor({ state: 'attached', timeout: 15_000 })
.then((): WalletPickerSurface => ({
kind: 'modal',
page: dappPage,
dappPage,
}))
.catch(() => null)

await connectButton.click()
return pickerPopup

const surface = await Promise.race([popupRace, modalRace])
if (!surface) {
throw new Error(
'wallet picker did not open as an in-page modal or a popup window'
)
}

if (surface.kind === 'modal') {
await modalHost
.locator('[role="dialog"]')
.waitFor({ state: 'visible', timeout: 5_000 })
}

return surface
}

export function walletPickerModalHost(dappPage: Page): Locator {
return dappPage.locator(WALLET_PICKER_MODAL_HOST)
}

export function walletPickerModalRowByTitle(
dappPage: Page,
title: string
): Locator {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return walletPickerModalHost(dappPage).getByRole('button', {
name: new RegExp(`^${escaped}(\\s+${escaped})?$`),
})
}
1 change: 1 addition & 0 deletions core/wallet-ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export * from './components/transaction-card.js'
export * from './components/transaction-detail.js'

export * from './windows/wallet-picker.js'
export * from './windows/wallet-picker-modal.js'
export * from './windows/popup.js'

export * from './handle-errors.js'
Expand Down
Loading