Skip to content

Commit

Permalink
feat(ui-test): cover email (#1684)
Browse files Browse the repository at this point in the history
  • Loading branch information
arein authored Jan 3, 2024
1 parent a4605c0 commit 5101280
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 10 deletions.
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.approveSign()
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 approveSign() {
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)
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

3 comments on commit 5101280

@vercel
Copy link

@vercel vercel bot commented on 5101280 Jan 3, 2024

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 5101280 Jan 3, 2024

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 5101280 Jan 3, 2024

Choose a reason for hiding this comment

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

Please sign in to comment.