diff --git a/apps/laboratory/playwright.config.ts b/apps/laboratory/playwright.config.ts index 082bb40ecf..2c9227a262 100644 --- a/apps/laboratory/playwright.config.ts +++ b/apps/laboratory/playwright.config.ts @@ -9,7 +9,6 @@ config({ path: './.env.local' }) export default defineConfig({ testDir: './tests', - testIgnore: 'email.spec.ts', fullyParallel: true, retries: getValue(2, 1), workers: getValue(8, 4), diff --git a/apps/laboratory/tests/email.spec.ts b/apps/laboratory/tests/email.spec.ts index 8a22536044..7fbc7474e0 100644 --- a/apps/laboratory/tests/email.spec.ts +++ b/apps/laboratory/tests/email.spec.ts @@ -1,52 +1,44 @@ -import { testMEmail } from './shared/fixtures/w3m-fixture' -import { DeviceRegistrationPage } from './shared/pages/DeviceRegistrationPage' -import { Email } from './shared/utils/email' +import { expect } from '@playwright/test' +import { testMEmail } from './shared/fixtures/w3m-email-fixture' +import { SECURE_WEBSITE_URL } from './shared/constants' -// Prevent collissions by using a semi-random reserved Mailsac email -const AVAILABLE_MAILSAC_ADDRESSES = 10 - -testMEmail.beforeEach(async ({ modalPage, context, modalValidator }) => { - // This is prone to collissions and will be improved later - const tempEmail = `web3modal${Math.floor( - Math.random() * AVAILABLE_MAILSAC_ADDRESSES - )}@mailsac.com` - const mailsacApiKey = process.env['MAILSAC_API_KEY'] - if (!mailsacApiKey) { - throw new Error('MAILSAC_API_KEY is not set') - } - const email = new Email(mailsacApiKey) - await email.deleteAllMessages(tempEmail) - await modalPage.loginWithEmail(tempEmail) - - let latestMessage = await email.getNewMessage(tempEmail) - let messageId = latestMessage._id +testMEmail.beforeEach(async ({ modalValidator }) => { + await modalValidator.expectConnected() +}) - if (!messageId) { - throw new Error('No messageId found') - } +testMEmail('it should sign', async ({ modalPage, modalValidator }) => { + await modalPage.sign() + await modalPage.approveSign() + await modalValidator.expectAcceptedSign() +}) - let otp = await email.getCodeFromEmail(tempEmail, messageId) +testMEmail('it should upgrade wallet', async ({ modalPage, context }) => { + const page = await modalPage.clickWalletUpgradeCard(context) + expect(page.url()).toContain(SECURE_WEBSITE_URL) + await page.close() +}) - if (otp.length !== 6) { - // We got a device registration link so let's register first - const drp = new DeviceRegistrationPage(await context.newPage(), otp) - drp.load() - await drp.approveDevice() +testMEmail('it should reject sign', async ({ modalPage, modalValidator }) => { + await modalPage.sign() + await modalPage.rejectSign() + await modalValidator.expectRejectedSign() +}) - latestMessage = await email.getNewMessage(tempEmail) - messageId = latestMessage._id - if (!messageId) { - throw new Error('No messageId found') - } - otp = await email.getCodeFromEmail(tempEmail, messageId) - } +testMEmail('it should switch network and sign', async ({ modalPage, modalValidator }) => { + let targetChain = 'Polygon' + await modalPage.switchNetwork(targetChain) + await modalValidator.expectNetwork(targetChain) + await modalPage.page.waitForTimeout(1500) + await modalPage.sign() + await modalPage.approveSign() + await modalValidator.expectAcceptedSign() - await modalPage.enterOTP(otp) - await modalValidator.expectConnected() -}) + await modalPage.page.waitForTimeout(2000) -testMEmail('it should sign', async ({ modalPage, modalValidator }) => { - testMEmail.skip(modalPage.library === 'wagmi', 'Tests are flaky on wagmi') + targetChain = 'Ethereum' + await modalPage.switchNetwork(targetChain) + await modalValidator.expectNetwork(targetChain) + await modalPage.page.waitForTimeout(1500) await modalPage.sign() await modalPage.approveSign() await modalValidator.expectAcceptedSign() diff --git a/apps/laboratory/tests/shared/constants/devices.ts b/apps/laboratory/tests/shared/constants/devices.ts index bd6f14150f..927e8f5b78 100644 --- a/apps/laboratory/tests/shared/constants/devices.ts +++ b/apps/laboratory/tests/shared/constants/devices.ts @@ -1 +1 @@ -export const DEVICES = ['Desktop Chrome', 'Desktop Brave', 'Desktop Firefox'] +export const DEVICES = ['Desktop Firefox', 'Desktop Brave', 'Desktop Chrome'] diff --git a/apps/laboratory/tests/shared/constants/index.ts b/apps/laboratory/tests/shared/constants/index.ts index fd5ec3badd..e42c4994b3 100644 --- a/apps/laboratory/tests/shared/constants/index.ts +++ b/apps/laboratory/tests/shared/constants/index.ts @@ -8,4 +8,5 @@ export const DEFAULT_SESSION_PARAMS: SessionParams = { optAccounts: ['1', '2'], accept: true } +export const SECURE_WEBSITE_URL = 'https://secure.walletconnect.com' export const DEFAULT_CHAIN_NAME = process.env['DEFAULT_CHAIN_NAME'] || 'Ethereum' diff --git a/apps/laboratory/tests/shared/fixtures/w3m-email-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-email-fixture.ts new file mode 100644 index 0000000000..a1f6a3dc7f --- /dev/null +++ b/apps/laboratory/tests/shared/fixtures/w3m-email-fixture.ts @@ -0,0 +1,61 @@ +import { test as base } from '@playwright/test' +import type { ModalFixture } from './w3m-fixture' +import { ModalPage } from '../pages/ModalPage' +import { ModalValidator } from '../validators/ModalValidator' +import { DeviceRegistrationPage } from '../pages/DeviceRegistrationPage' +import { Email } from '../utils/email' + +const mailsacApiKey = process.env['MAILSAC_API_KEY'] +if (!mailsacApiKey) { + throw new Error('MAILSAC_API_KEY is not set') +} + +export const testMEmail = base.extend({ + library: ['wagmi', { option: true }], + modalPage: async ({ page, library, context }, use, testInfo) => { + const modalPage = new ModalPage(page, library, 'email') + await modalPage.load() + + const email = new Email(mailsacApiKey) + + const tempEmail = email.getEmailAddressToUse(testInfo.parallelIndex) + + await email.deleteAllMessages(tempEmail) + await modalPage.loginWithEmail(tempEmail) + + let messageId = await email.getLatestMessageId(tempEmail) + + if (!messageId) { + throw new Error('No messageId found') + } + let emailBody = await email.getEmailBody(tempEmail, messageId) + let otp = '' + if (email.isApproveEmail(emailBody)) { + const url = email.getApproveUrlFromBody(emailBody) + + await email.deleteAllMessages(tempEmail) + + const drp = new DeviceRegistrationPage(await context.newPage(), url) + drp.load() + await drp.approveDevice() + await drp.close() + + messageId = await email.getLatestMessageId(tempEmail) + + emailBody = await email.getEmailBody(tempEmail, messageId) + if (!email.isApproveEmail(emailBody)) { + otp = email.getOtpCodeFromBody(emailBody) + } + } + if (otp.length !== 6) { + otp = email.getOtpCodeFromBody(emailBody) + } + await modalPage.enterOTP(otp) + + await use(modalPage) + }, + modalValidator: async ({ modalPage }, use) => { + const modalValidator = new ModalValidator(modalPage.page) + await use(modalValidator) + } +}) diff --git a/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts index 0604268021..a7dc86b164 100644 --- a/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts +++ b/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts @@ -34,16 +34,5 @@ export const testMSiwe = base.extend({ await use(modalValidator) } }) -export const testMEmail = base.extend({ - library: ['wagmi', { option: true }], - modalPage: async ({ page, library }, use) => { - const modalPage = new ModalPage(page, library, 'email') - await modalPage.load() - await use(modalPage) - }, - modalValidator: async ({ modalPage }, use) => { - const modalValidator = new ModalValidator(modalPage.page) - await use(modalValidator) - } -}) + export { expect } from '@playwright/test' diff --git a/apps/laboratory/tests/shared/fixtures/w3m-wallet-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-wallet-fixture.ts index 8edd267d47..87d6f25c16 100644 --- a/apps/laboratory/tests/shared/fixtures/w3m-wallet-fixture.ts +++ b/apps/laboratory/tests/shared/fixtures/w3m-wallet-fixture.ts @@ -1,8 +1,9 @@ import { testM as base, testMSiwe as siwe } from './w3m-fixture' import { WalletPage } from '../pages/WalletPage' import { WalletValidator } from '../validators/WalletValidator' -import type { BrowserContext, Page } from '@playwright/test' + import { DEFAULT_SESSION_PARAMS } from '../constants' +import { doActionAndWaitForNewPage } from '../utils/actions' // Declare the types of fixtures to use interface ModalWalletFixture { @@ -35,18 +36,4 @@ export const testMWSiwe = siwe.extend({ } }) -export async function doActionAndWaitForNewPage( - action: Promise, - context: BrowserContext -): Promise { - if (!context) { - throw new Error('Browser Context is undefined') - } - const pagePromise = context.waitForEvent('page') - await action - const newPage = await pagePromise - - return newPage -} - export { expect } from '@playwright/test' diff --git a/apps/laboratory/tests/shared/pages/DeviceRegistrationPage.ts b/apps/laboratory/tests/shared/pages/DeviceRegistrationPage.ts index 6afd0b48c0..dad06d1d4a 100644 --- a/apps/laboratory/tests/shared/pages/DeviceRegistrationPage.ts +++ b/apps/laboratory/tests/shared/pages/DeviceRegistrationPage.ts @@ -1,4 +1,6 @@ -import type { Page } from '@playwright/test' +import { expect, type Page } from '@playwright/test' + +const LOGIN_APPROVED_SUCCESS_TEXT = 'Login Approved' export class DeviceRegistrationPage { constructor( @@ -8,9 +10,14 @@ export class DeviceRegistrationPage { async load() { await this.page.goto(this.url) + await this.page.waitForLoadState() } async approveDevice() { await this.page.getByRole('button', { name: 'Approve' }).click() + await expect(this.page.getByText(LOGIN_APPROVED_SUCCESS_TEXT)).toBeVisible() + } + async close() { + await this.page.close() } } diff --git a/apps/laboratory/tests/shared/pages/ModalPage.ts b/apps/laboratory/tests/shared/pages/ModalPage.ts index 9c7aebe1fd..27878f25ed 100644 --- a/apps/laboratory/tests/shared/pages/ModalPage.ts +++ b/apps/laboratory/tests/shared/pages/ModalPage.ts @@ -1,6 +1,8 @@ -import type { Locator, Page } from '@playwright/test' +/* eslint-disable no-await-in-loop */ +import type { BrowserContext, Locator, Page } from '@playwright/test' import { expect } from '@playwright/test' import { BASE_URL } from '../constants' +import { doActionAndWaitForNewPage } from '../utils/actions' export type ModalFlavor = 'default' | 'siwe' | 'email' @@ -64,16 +66,25 @@ export class ModalPage { async enterOTP(otp: string) { const splitted = otp.split('') + // Remove empy space in OTP code 111 111 + splitted.splice(3, 1) + // eslint-disable-next-line no-plusplus for (let i = 0; i < splitted.length; i++) { const digit = splitted[i] if (!digit) { throw new Error('Invalid OTP') } - /* eslint-disable no-await-in-loop */ - await this.page.getByTestId('wui-otp-input').locator('input').nth(i).focus() - /* eslint-disable no-await-in-loop */ - await this.page.getByTestId('wui-otp-input').locator('input').nth(i).fill(digit) + const otpInput = this.page.getByTestId('wui-otp-input') + const wrapper = otpInput.locator('wui-input-numeric').nth(i) + await expect(wrapper, `Wrapper element for input ${i} should be visible`).toBeVisible({ + timeout: 5000 + }) + const input = wrapper.locator('input') + await expect(input, `Input ${i} should be enabled`).toBeEnabled({ + timeout: 5000 + }) + await input.fill(digit) } await expect(this.page.getByText('Confirm Email')).not.toBeVisible() @@ -94,16 +105,39 @@ export class ModalPage { await this.page.getByTestId('sign-message-button').click() } - async approveSign() { + async signatureRequestFrameShouldVisible() { await expect( this.page.frameLocator('#w3m-iframe').getByText('requests a signature'), 'Web3Modal iframe should be visible' - ).toBeVisible() + ).toBeVisible({ + timeout: 10000 + }) await this.page.waitForTimeout(2000) - await this.page - .frameLocator('#w3m-iframe') - .getByRole('button', { name: 'Sign', exact: true }) - .click() + } + async clickSignatureRequestButton(name: string) { + await this.page.frameLocator('#w3m-iframe').getByRole('button', { name, exact: true }).click() + } + + async approveSign() { + await this.signatureRequestFrameShouldVisible() + await this.clickSignatureRequestButton('Sign') + } + + async rejectSign() { + await this.signatureRequestFrameShouldVisible() + await this.clickSignatureRequestButton('Cancel') + } + + async clickWalletUpgradeCard(context: BrowserContext) { + await this.page.getByTestId('account-button').click() + await this.page.getByTestId('w3m-wallet-upgrade-card').click() + + const page = await doActionAndWaitForNewPage( + this.page.getByTestId('w3m-secure-website-button').click(), + context + ) + + return page } async promptSiwe() { diff --git a/apps/laboratory/tests/shared/utils/actions.ts b/apps/laboratory/tests/shared/utils/actions.ts new file mode 100644 index 0000000000..84b9880cb6 --- /dev/null +++ b/apps/laboratory/tests/shared/utils/actions.ts @@ -0,0 +1,15 @@ +import type { BrowserContext, Page } from '@playwright/test' + +export async function doActionAndWaitForNewPage( + action: Promise, + context: BrowserContext +): Promise { + if (!context) { + throw new Error('Browser Context is undefined') + } + const pagePromise = context.waitForEvent('page') + await action + const newPage = await pagePromise + + return newPage +} diff --git a/apps/laboratory/tests/shared/utils/email.ts b/apps/laboratory/tests/shared/utils/email.ts index 565bff58e4..096333aaf3 100644 --- a/apps/laboratory/tests/shared/utils/email.ts +++ b/apps/laboratory/tests/shared/utils/email.ts @@ -1,67 +1,87 @@ -import { Mailsac, type EmailMessage } from '@mailsac/api' +import { Mailsac } from '@mailsac/api' +const EMAIL_CHECK_TIMEOUT = 1000 +const MAX_EMAIL_CHECK = 16 +const EMAIL_APPROVE_BUTTON_TEXT = 'Approve this login' +const APPROVE_URL_REGEX = /https:\/\/register.*/u +const OTP_CODE_REGEX = /\d{3}\s?\d{3}/u +const AVAILABLE_MAILSAC_ADDRESSES = 10 export class Email { // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly mailsac: Mailsac - private messageCount: number constructor(public readonly apiKey: string) { this.mailsac = new Mailsac({ headers: { 'Mailsac-Key': apiKey } }) - this.messageCount = 0 } async deleteAllMessages(email: string) { - this.messageCount = 0 - return await this.mailsac.messages.deleteAllMessages(email) } - async getNewMessage(email: string) { - const timeout = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Timeout waiting for email')), 15000) + timeout(ms: number) { + return new Promise(resolve => { + setTimeout(resolve, ms) }) + } - const messagePoll = new Promise(resolve => { - const interval = setInterval(async () => { - const messages = await this.mailsac.messages.listMessages(email) - if (messages.data.length > 0 && messages.data.length > this.messageCount) { - clearInterval(interval) - this.messageCount = messages.data.length - const message = messages.data[0] - - if (!message) { - throw new Error('No message found') - } - - return resolve(message) + async getLatestMessageId(email: string): Promise { + let checks = 0 + /* eslint-disable no-await-in-loop */ + while (checks < MAX_EMAIL_CHECK) { + const messages = await this.mailsac.messages.listMessages(email) + if (messages.data.length > 0) { + const message = messages.data[0] + if (!message) { + throw new Error('No message found') + } + const id = message._id + if (!id) { + throw new Error('Message ID not present') } - return undefined - }, 500) - }) - - return Promise.any([timeout, messagePoll]) + return id + } + await this.timeout(EMAIL_CHECK_TIMEOUT) + checks += 1 + } + throw new Error('No email found') } - async getCodeFromEmail(email: string, messageId: string) { + async getEmailBody(email: string, messageId: string): Promise { const result = await this.mailsac.messages.getBodyPlainText(email, messageId) - if (result.data.includes('Approve this login')) { - // Get the register.web3modal.com device registration URL - const regex = /https:\/\/register.*/u - const match = result.data.match(regex) - if (match) { - return match[0] - } + return result.data + } + + isApproveEmail(body: string): boolean { + return body.includes(EMAIL_APPROVE_BUTTON_TEXT) + } - throw new Error(`No url found in email: ${result.data}`) + getApproveUrlFromBody(body: string): string { + const match = body.match(APPROVE_URL_REGEX) + if (match) { + return match[0] } - const otpRegex = /\d{3}\s?\d{3}/u - const match = result.data.match(otpRegex) + throw new Error(`No url found in email: ${body}`) + } + + getOtpCodeFromBody(body: string): string { + const match = body.match(OTP_CODE_REGEX) if (match) { - return match[0].replace(/\s/gu, '') + return match[0] + } + + throw new Error(`No code found in email: ${body}`) + } + + getEmailAddressToUse(index: number): string { + const maxIndex = AVAILABLE_MAILSAC_ADDRESSES - 1 + if (index > maxIndex) { + throw new Error( + `No available Mailsac address. Requested index ${index}, maximum: ${maxIndex}` + ) } - throw new Error(`No code found in email: ${result.data}`) + return `web3modal${index}@mailsac.com` } } diff --git a/apps/laboratory/tests/shared/utils/project.ts b/apps/laboratory/tests/shared/utils/project.ts index 2474435b74..36173d0c92 100644 --- a/apps/laboratory/tests/shared/utils/project.ts +++ b/apps/laboratory/tests/shared/utils/project.ts @@ -35,9 +35,11 @@ const braveOptions: UseOptions = { const customProjectProperties: CustomProjectProperties = { 'Desktop Brave/wagmi': { + testIgnore: 'email.spec.ts', useOptions: braveOptions }, 'Desktop Brave/ethers': { + testIgnore: 'email.spec.ts', useOptions: braveOptions }, 'Desktop Chrome/wagmi': { @@ -45,9 +47,6 @@ const customProjectProperties: CustomProjectProperties = { }, 'Desktop Firefox/wagmi': { testIgnore: 'email.spec.ts' - }, - 'Desktop Safari/wagmi': { - testIgnore: 'email.spec.ts' } } diff --git a/apps/laboratory/tests/shared/validators/ModalValidator.ts b/apps/laboratory/tests/shared/validators/ModalValidator.ts index 509e7b5d0d..dd3118dc91 100644 --- a/apps/laboratory/tests/shared/validators/ModalValidator.ts +++ b/apps/laboratory/tests/shared/validators/ModalValidator.ts @@ -45,6 +45,16 @@ export class ModalValidator { }) } + async expectNetwork(network: string) { + const networkButton = this.page.locator('wui-network-button') + await expect(networkButton, `Network button should contain text ${network}`).toHaveText( + network, + { + timeout: 5000 + } + ) + } + async expectAcceptedSign() { // We use Chakra Toast and it's not quite straightforward to set the `data-testid` attribute on the toast element. await expect(this.page.getByText(ConstantsUtil.SigningSucceededToastTitle)).toBeVisible({ diff --git a/packages/scaffold/src/views/w3m-account-view/index.ts b/packages/scaffold/src/views/w3m-account-view/index.ts index 0e64b0bc25..c8500ff45b 100644 --- a/packages/scaffold/src/views/w3m-account-view/index.ts +++ b/packages/scaffold/src/views/w3m-account-view/index.ts @@ -195,6 +195,7 @@ export class W3mAccountView extends LitElement { label="Upgrade your wallet" description="Transition to a self-custodial wallet" icon="wallet" + data-testid="w3m-wallet-upgrade-card" > ` } diff --git a/packages/scaffold/src/views/w3m-upgrade-wallet-view/index.ts b/packages/scaffold/src/views/w3m-upgrade-wallet-view/index.ts index 353a3c3eb0..508c7be5f3 100644 --- a/packages/scaffold/src/views/w3m-upgrade-wallet-view/index.ts +++ b/packages/scaffold/src/views/w3m-upgrade-wallet-view/index.ts @@ -14,6 +14,7 @@ export class W3mUpgradeWalletView extends LitElement { variant="fill" href=${ConstantsUtil.SECURE_SITE_DASHBOARD} imageSrc=${ConstantsUtil.SECURE_SITE_FAVICON} + data-testid="w3m-secure-website-button" >