Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui-test): cover email #1684

Merged
merged 17 commits into from
Jan 3, 2024
1 change: 1 addition & 0 deletions .github/workflows/ui_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
env:
NEXT_PUBLIC_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_PROJECT_ID }}
NEXTAUTH_SECRET: ${{ secrets.TESTS_NEXTAUTH_SECRET }}
MAILSAC_API_KEY: ${{ secrets.TESTS_MAILSEC_API_KEY }}
CI: true
working-directory: ./apps/laboratory/
run: npm run playwright:test
Expand Down
1 change: 1 addition & 0 deletions apps/laboratory/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Obtain a project ID from https://cloud.walletconnect.com
NEXT_PUBLIC_PROJECT_ID=""
NEXTAUTH_SECRET=""
MAILSAC_API_KEY=""
3 changes: 2 additions & 1 deletion apps/laboratory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"devDependencies": {
"@playwright/test": "1.40.1",
"dotenv": "16.3.1",
"ethers": "6.9.0"
"ethers": "6.9.0",
"@mailsac/api": "1.0.5"
}
}
6 changes: 3 additions & 3 deletions apps/laboratory/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { BASE_URL } from './tests/shared/constants'

import { config } from 'dotenv'
import type { ModalFixture } from './tests/shared/fixtures/w3m-fixture'
config({ path: './.env.local' })
config({ path: './.env' })

export default defineConfig<ModalFixture>({
testDir: './tests',

fullyParallel: true,
retries: process.env['CI'] ? 2 : 0,
workers: process.env['CI'] ? 1 : undefined,
retries: 0,
workers: 1,
reporter: [['list'], ['html']],

expect: {
Expand Down
57 changes: 57 additions & 0 deletions apps/laboratory/tests/email.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { testMEmail } from './shared/fixtures/w3m-fixture'
import { DeviceRegistrationPage } from './shared/pages/DeviceRegistrationPage'
import { Email } from './shared/utils/email'

// Prevent collissions by using a semi-random reserved Mailsac email
const AVAILABLE_MAILSAC_ADDRESSES = 10

testMEmail.beforeEach(async ({ modalPage, context, modalValidator }) => {
// Skip wagmi as it's not working
if (modalPage.library === 'wagmi') {
return
}
// 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

if (!messageId) {
throw new Error('No messageId found')
}

let otp = await email.getCodeFromEmail(tempEmail, messageId)

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()

latestMessage = await email.getNewMessage(tempEmail)
messageId = latestMessage._id
if (!messageId) {
throw new Error('No messageId found')
}
otp = await email.getCodeFromEmail(tempEmail, messageId)
}

await modalPage.enterOTP(otp)
await modalValidator.expectConnected()
})

testMEmail('it should sign', async ({ modalPage, modalValidator }) => {
testMEmail.skip(modalPage.library === 'wagmi', 'Tests are flaky on wagmi')
await modalPage.sign()
await modalPage.appoveSign()
arein marked this conversation as resolved.
Show resolved Hide resolved
await modalValidator.expectAcceptedSign()
})
12 changes: 12 additions & 0 deletions apps/laboratory/tests/shared/fixtures/w3m-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,16 @@ export const testMSiwe = base.extend<ModalFixture>({
await use(modalValidator)
}
})
export const testMEmail = base.extend<ModalFixture>({
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'
16 changes: 16 additions & 0 deletions apps/laboratory/tests/shared/pages/DeviceRegistrationPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Page } from '@playwright/test'

export class DeviceRegistrationPage {
constructor(
public readonly page: Page,
public readonly url: string
) {}

async load() {
await this.page.goto(this.url)
}

async approveDevice() {
await this.page.getByRole('button', { name: 'Approve' }).click()
}
}
49 changes: 45 additions & 4 deletions apps/laboratory/tests/shared/pages/ModalPage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { BASE_URL } from '../constants'

export type ModalFlavor = 'default' | 'siwe'
export type ModalFlavor = 'default' | 'siwe' | 'email'

export class ModalPage {
private readonly baseURL = BASE_URL
Expand All @@ -16,9 +17,9 @@ export class ModalPage {
) {
this.connectButton = this.page.getByTestId('connect-button')
this.url =
flavor === 'siwe'
? `${this.baseURL}library/${this.library}-siwe/`
: `${this.baseURL}library/${this.library}/`
flavor === 'default'
? `${this.baseURL}library/${this.library}/`
: `${this.baseURL}library/${this.library}-${this.flavor}/`
}

async load() {
Expand All @@ -33,6 +34,35 @@ export class ModalPage {
await this.page.getByTestId('copy-wc2-uri').click()
}

async loginWithEmail(email: string) {
await this.page.goto(this.url)
// Connect Button doesn't have a proper `disabled` attribute so we need to wait for the button to change the text
await this.page
.getByTestId('connect-button')
.getByRole('button', { name: 'Connect Wallet' })
.click()
await this.page.getByTestId('wui-email-input').locator('input').focus()
await this.page.getByTestId('wui-email-input').locator('input').fill(email)
await this.page.getByTestId('wui-email-input').locator('input').press('Enter')
}

async enterOTP(otp: string) {
const splitted = otp.split('')
// 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)
}

await expect(this.page.getByText('Confirm Email')).not.toBeVisible()
}

async disconnect() {
await this.page.getByTestId('account-button').click()
await this.page.getByTestId('disconnect-button').click()
Expand All @@ -42,6 +72,17 @@ export class ModalPage {
await this.page.getByTestId('sign-message-button').click()
}

async appoveSign() {
arein marked this conversation as resolved.
Show resolved Hide resolved
await expect(
this.page.frameLocator('#w3m-iframe').getByText('requests a signature')
).toBeVisible()
await this.page.waitForTimeout(2000)
await this.page
.frameLocator('#w3m-iframe')
.getByRole('button', { name: 'Sign', exact: true })
.click()
}

async promptSiwe() {
await this.page.getByTestId('w3m-connecting-siwe-sign').click()
}
Expand Down
67 changes: 67 additions & 0 deletions apps/laboratory/tests/shared/utils/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Mailsac, type EmailMessage } from '@mailsac/api'

export class Email {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly mailsac: Mailsac<any>
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<EmailMessage>((_, reject) => {
setTimeout(() => reject(new Error('Timeout waiting for email')), 15000)
})

const messagePoll = new Promise<EmailMessage>(resolve => {
const interval = setInterval(async () => {
const messages = await this.mailsac.messages.listMessages(email)
arein marked this conversation as resolved.
Show resolved Hide resolved
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)
}

return undefined
}, 500)
})

return Promise.any([timeout, messagePoll])
}

async getCodeFromEmail(email: string, messageId: string) {
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]
}

throw new Error(`No url found in email: ${result.data}`)
}

const otpRegex = /\d{3}\s?\d{3}/u
const match = result.data.match(otpRegex)
if (match) {
return match[0].replace(/\s/gu, '')
}

throw new Error(`No code found in email: ${result.data}`)
}
}
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/ui/src/composites/wui-email-input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class WuiEmailInput extends LitElement {
size="md"
.disabled=${this.disabled}
.value=${this.value}
data-testid="wui-email-input"
></wui-input-text>
${this.templateError()}
`
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/composites/wui-otp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class WuiOtp extends LitElement {
// -- Render -------------------------------------------- //
public override render() {
return html`
<wui-flex gap="xxs">
<wui-flex gap="xxs" data-testid="wui-otp-input">
${Array.from({ length: this.length }).map(
(_, index: number) => html`
<wui-input-numeric
Expand Down
Loading