feat(e2e): Transfers FE e2e tests (#192)

This commit is contained in:
Juan Enrique Alcaraz 2024-09-12 18:48:14 +02:00 committed by GitHub
parent 4f2bcea5be
commit d2225f1b70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 451 additions and 128 deletions

View file

@ -18,7 +18,6 @@ Install rust by executing a script from the internet (😅):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
Check the version with `cargo version`.
Finally add the wasm target:
@ -242,37 +241,7 @@ data come through.
### Run the Frontend
Now on your own machine, checkout the
https://github.com/informalsystems/cycles-hackathon-app.
Copy the `.env.example` file to `.env.local`:
```bash
cp .env.example .env.local
```
and set the relevant fields. You should have the contract address and TEE pubkey
from the output of the `deploy.sh` and `handshake.sh` scripts, respectfully. The
chain id is probably `testing` and the IP address for the URLs is probably
`143.244.186.205`. Modify accordingly. For example:
```bash
#.env.local
NEXT_PUBLIC_TRANSFERS_CONTRACT_ADDRESS=wasm1ch9ed27cdu3a4fkx37gnagm7jcthj0rggnmmjwwwe4xhwmk0d65q8fn9pz
NEXT_PUBLIC_ENCLAVE_PUBLIC_KEY=030c25e39743fd4c7553d87873919281d567b5c328fb903cbfbe9541518736a2d2
NEXT_PUBLIC_CHAIN_ID=testing
NEXT_PUBLIC_CHAIN_RPC_URL=http://143.244.186.205:26657
NEXT_PUBLIC_CHAIN_REST_URL=http://143.244.186.205:1317
```
Install and run the app:
```bash
npm install -f
npm run dev
```
You can now open the app in http://localhost:3000/.
Now on your own machine, follow [these steps](./frontend/README.md#development).
Make sure you have Keplr installed in your browser and you should now be able to
use the app!

View file

@ -1,9 +1,13 @@
# App required variables
NEXT_PUBLIC_TARGET_CHAIN=localWasm
NEXT_PUBLIC_ENCLAVE_PUBLIC_KEY=02360955ff74750f6ea0b539f41cce89451f591e4c835d0a5406e6effa96dd169d
NEXT_PUBLIC_TRANSFERS_CONTRACT_ADDRESS=wasm1jfgr0vgunezkhfmdy7krrupu6yjhx224nxtjptll2ylkkqhyzeshrspu9
# E2E Testing
# E2E testing required variables
TEST_BASE_URL=http://127.0.0.1:3000
TEST_KEPLR_EXTENSION_VERSION=0.12.124
TEST_WALLET_MNEMONIC=debris topic trash february punch advance tackle alert reduce box chase lend buffalo effort napkin drip mountain result rely swear tornado master devote what
TEST_SECONDARY_WALLET_MNEMONIC=employ jungle nuclear clutch general vicious thrive width time asthma shadow orchard wage affair matrix slush room weapon prize that record path grit tourist
TEST_SECONDARY_WALLET_ADDRESS=wasm1lejkm8nevz4hgmafyfm03upkkj76cvzj4geapv
TEST_WALLET_PASSWORD=;pzPCXB^@92byC
PLAYWRIGHT_HTML_OPEN=never

View file

@ -1,13 +1,13 @@
# Transfer App
This is an example frontend that illustrates how to interact with a Transfer Quartz App.
This is an example frontend that illustrates how to interact with a Quartz app.
This example offers:
This example offers the ability to:
- Deposit amounts into a balance
- Withdraw the whole deposit
- Transfer amounts between wallet addresses in a private-preserving way
- Query your encrypted balance to capture changes
- Transfer amounts between wallets in a private-preserving way
- Query your encrypted balance
- Switch between Keplr wallets
## Requirements
@ -26,7 +26,7 @@ Install dependencies:
npm ci
```
The App requires some environment variables to fully work. Be sure to set up those accordingly to your local environment.
The app requires some environment variables to fully work. Be sure to set up those accordingly to your local environment.
You should start from the template:
@ -34,10 +34,52 @@ You should start from the template:
cp .env.example .env.local
```
Required environment variables:
```
# Choose target chain configuration
NEXT_PUBLIC_TARGET_CHAIN=<localWasm | localNeutron | doWasm>
# Enclave public key to encrypt transfers
NEXT_PUBLIC_ENCLAVE_PUBLIC_KEY=<public_key>
# Target transfers contract
NEXT_PUBLIC_TRANSFERS_CONTRACT_ADDRESS=<contract_address>
```
Run the app:
```bash
npm run dev
```
And now everything is up & running 🎉
App will be running on http://localhost:3000/ and now everything is up & running 🎉
## E2E Testing
For tests to work, you need to set up the following required environment variables:
```
# Frontend base url
TEST_BASE_URL=<url>
# Keplr browser extension version
TEST_KEPLR_EXTENSION_VERSION=<version>
# Main wallet mnemonic (Use only funded wallets)
TEST_WALLET_MNEMONIC=<mnemonic>
# Secondary wallet mnemonic (Use only funded wallets)
TEST_SECONDARY_WALLET_MNEMONIC=<mnemonic>
# Secondary wallet address
TEST_SECONDARY_WALLET_ADDRESS=<wallet_address>
# Keplr wallet password. It can be whatever
TEST_WALLET_PASSWORD=<password>
```
Run all E2E tests:
```bash
npm run test
```
If want to run the tests with the Playwright dedicated interface, run:
```bash
npm run test:ui
```

View file

@ -3,5 +3,11 @@ namespace NodeJS {
NEXT_PUBLIC_ENCLAVE_PUBLIC_KEY: string
NEXT_PUBLIC_TARGET_CHAIN: string
NEXT_PUBLIC_TRANSFERS_CONTRACT_ADDRESS: string
TEST_BASE_URL: string
TEST_KEPLR_EXTENSION_VERSION: string
TEST_WALLET_MNEMONIC: string
TEST_SECONDARY_WALLET_MNEMONIC: string
TEST_SECONDARY_WALLET_ADDRESS: string
TEST_WALLET_PASSWORD: string
}
}

View file

@ -9,7 +9,8 @@
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "playwright test",
"test:ui": "playwright test --ui"
"test:ui": "playwright test --ui",
"prepare": "npx playwright install chromium"
},
"dependencies": {
"eciesjs": "^0.4.7",

View file

@ -25,6 +25,8 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
timeout: 0,
globalTimeout: process.env.CI ? 5 * 60 * 1000 : undefined,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */

View file

@ -22,6 +22,7 @@ export default function Landing() {
<main className="flex min-h-screen flex-col items-center gap-4 p-24">
<p>Connect your Keplr wallet to log in</p>
<StyledText
as="button"
variant="button.primary"
onClick={connectWallet}
>

View file

@ -30,12 +30,14 @@ export default function SetSeed() {
</code>
<div className="flex flex-col gap-4">
<StyledText
as="button"
variant="button.primary"
onClick={acceptPhrase}
>
Continue with the autogenerated seed phrase
</StyledText>
<StyledText
as="button"
variant="button.secondary"
onClick={() => setIsModalOpen(true)}
>

View file

@ -96,7 +96,7 @@ export default function Dashboard() {
).then((data) => {
setLoading(false)
setBalance(retrieveBalance(data))
showSuccess('Balance updated correctly')
showSuccess('Balance updated successfully')
})
}
},
@ -167,6 +167,7 @@ export default function Dashboard() {
<StyledText
className="w-full justify-start bg-emerald-500"
as="button"
variant="button.primary"
onClick={() => setIsDepositModalOpen(true)}
>
@ -174,6 +175,7 @@ export default function Dashboard() {
Deposit
</StyledText>
<StyledText
as="button"
variant="button.primary"
className="w-full justify-start bg-violet-500"
onClick={() => setIsTransferModalOpen(true)}
@ -182,6 +184,7 @@ export default function Dashboard() {
Transfer
</StyledText>
<StyledText
as="button"
className="w-full justify-start bg-amber-500"
variant="button.primary"
onClick={() => setIsWithdrawModalOpen(true)}
@ -192,6 +195,7 @@ export default function Dashboard() {
<div className="my-1 w-full border-black/25"></div>
<StyledText
className="w-full justify-start"
as="button"
variant="button.secondary"
onClick={() => {
const res = confirm(

View file

@ -101,6 +101,7 @@ export function DepositModalWindow(props: ModalWindowProps) {
Deposit
</StyledText>
<StyledText
as="button"
variant="button.secondary"
onClick={props.onClose}
>

View file

@ -66,6 +66,7 @@ export function EnterSeedModal({
Continue
</StyledText>
<StyledText
as="button"
variant="button.secondary"
onClick={onClose}
>

View file

@ -100,7 +100,7 @@ export function ModalWindow({
onTransitionEnd={handleTransitionEnd}
{...otherProps}
>
{children}
{isOpen && children}
</div>
</>,
document.body,

View file

@ -154,6 +154,7 @@ export function TransferModalWindow(props: ModalWindowProps) {
Transfer
</StyledText>
<StyledText
as="button"
variant="button.secondary"
onClick={props.onClose}
>

View file

@ -59,6 +59,7 @@ export function WithdrawModalWindow(props: ModalWindowProps) {
Withdraw
</StyledText>
<StyledText
as="button"
variant="button.secondary"
onClick={props.onClose}
>

View file

@ -0,0 +1,7 @@
type Url = 'landing' | 'seed' | 'dashboard'
export const routes: Record<Url, string> = {
landing: '/',
seed: '/set-seed',
dashboard: '/dashboard',
}

View file

@ -0,0 +1,39 @@
import { routes } from '@/config/routes'
import test from './fixtures'
import { connectWallet } from './helpers/connectWalet'
import { setSeedPhrase } from './helpers/setSeedPhrase'
const { dashboard, landing, seed } = routes
test.describe('Auth', () => {
test('can go nowhere but landing page without a wallet', async ({ page }) => {
await page.goto(seed)
await page.goto(dashboard)
await test
.expect(page.getByRole('button', { name: /connect/i }))
.toBeVisible()
})
test('can go nowhere but seed page without a seed phrase', async ({
context,
page,
}) => {
await connectWallet({ context, page })
await page.goto(landing)
await page.goto(dashboard)
await test.expect(page.getByText(/recovery seed phrase/i)).toBeVisible()
})
test('cannot go to anon pages once fully logged in', async ({
context,
page,
}) => {
await connectWallet({ context, page })
await setSeedPhrase({ page })
await page.goto(landing)
await page.goto(seed)
await test.expect(page.getByText(/balance:/i)).toBeVisible()
})
})

View file

@ -1,88 +1,49 @@
import {
test as baseTest,
chromium,
expect,
BrowserContext,
} from '@playwright/test'
import { test as baseTest, chromium } from '@playwright/test'
import path from 'path'
import { importWallet } from './helpers/importWallet'
let extensionUrl: string
const pathToExtension = path.join(
__dirname,
'extensions',
`keplr-extension-manifest-v3-v${process.env.TEST_KEPLR_EXTENSION_VERSION}`,
)
// Tests fixtures
const test = baseTest.extend<{}, { _globalContext: BrowserContext }>({
// Shared context for tests so Keplr initialization runs only once for all tests
_globalContext: [
async ({}, use) => {
const mnemonicWords = process.env.TEST_KEPLR_MNEMONIC!.split(' ')
const pathToExtension = path.join(
__dirname,
'extensions',
`keplr-extension-manifest-v3-v${process.env.TEST_KEPLR_EXTENSION_VERSION}`,
)
// We launch browser with the extension
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
})
const page = await context.waitForEvent('page')
const extensionId = /\/\/(.*?)\//.exec(page.url())![1]
const test = baseTest.extend<{
extensionUrl: string
}>({
// Overwritten Playwright context to setup Keplr wallet before all tests
context: async ({}, use) => {
// Launch browser with Keplr installed
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
})
// Keplr import wallet flow
await page.waitForURL(new RegExp(`${extensionId}/register.html`))
await expect(page.getByText('Import an existing wallet')).toBeVisible()
await page
.getByRole('button', { name: 'Import an existing wallet' })
.click()
await expect(
page.getByText('Use recovery phrase or private key'),
).toBeVisible()
await page
.getByRole('button', { name: 'Use recovery phrase or private key' })
.click()
const page = await context.waitForEvent('page')
await page.getByText('24 Words').click()
const seedInputs = await page.locator('input')
for (let i = 0; i < mnemonicWords.length; i++) {
await seedInputs.nth(i).fill(mnemonicWords[i])
}
await page.getByRole('button', { name: 'Import', exact: true }).click()
// Retrieve target URL to interact with Keplr extension
const extensionId = /\/\/(.*?)\//.exec(page.url())![1]
extensionUrl = `chrome-extension://${extensionId}`
await page
.getByPlaceholder('e.g. Trading, NFT Vault, Investment')
.fill('Playwright Wallet')
const inputs = await page.getByPlaceholder(
'At least 8 characters in length',
)
for (let i = 0; i < (await inputs.count()); i++) {
await inputs.nth(i).fill(process.env.TEST_KEPLR_PASSWORD!)
}
await page.getByRole('button', { name: 'Next' }).click()
// Import a wallet to be used in tests
await importWallet({
extensionUrl,
mnemonic: process.env.TEST_WALLET_MNEMONIC,
name: 'main',
page,
})
await expect(page.getByText('Select Chains')).toBeVisible()
await page.getByRole('button', { name: 'Save' }).click()
// Accept app suggested testnet info
await page.goto('/')
const addChainPage = await context.waitForEvent('page')
await addChainPage.getByRole('button', { name: 'Approve' }).click()
// Wait for App to load
await test.expect(page.getByText('Balance:')).toBeVisible()
await use(context)
await context.close()
},
{ scope: 'worker' },
],
context: async ({ _globalContext }, use) => {
await use(_globalContext)
await use(context)
await context.close()
},
page: async ({ context }, use) => {
const page = await context.newPage()
await use(page)
await page.close()
extensionUrl: async ({}, use) => {
await use(extensionUrl)
},
})

View file

@ -0,0 +1,21 @@
import { BrowserContext, Page } from '@playwright/test'
import { routes } from '@/config/routes'
export const connectWallet = async ({
context,
page,
}: {
context: BrowserContext
page: Page
}) => {
// Connect to Keplr wallet
await page.goto(routes.landing)
await page.getByRole('button', { name: /connect/i }).click()
// Accept app suggested testnet info
const addChainPage = await context.waitForEvent('page')
await addChainPage.getByRole('button', { name: /approve/i }).click()
await addChainPage.waitForEvent('close')
}

View file

@ -0,0 +1,20 @@
import { BrowserContext, Page } from '@playwright/test'
import { signTx } from './signTx'
export const getBalance = async ({
context,
page,
}: {
context: BrowserContext
page: Page
}) => {
// Check new balance
await page.getByRole('button', { name: /get/i }).click()
await signTx({ context, page })
// Wait for the success alert to appear so we know balance updated
await page.getByText(/\$/i).waitFor({ state: 'visible' })
return page.getByText(/\$/i).textContent()
}

View file

@ -0,0 +1,41 @@
import { Page } from '@playwright/test'
export const importWallet = async ({
extensionUrl,
mnemonic,
name,
page,
}: {
extensionUrl: string
mnemonic: string
name: string
page: Page
}) => {
await page.goto(`${extensionUrl}/register.html`)
const mnemonicWords = mnemonic.split(' ')
await page.getByRole('button', { name: /import/i }).click()
await page.getByRole('button', { name: /use/i }).click()
await page.getByRole('button', { name: /24/ }).click()
const seedInputs = await page.locator('input')
for (let i = 0; i < mnemonicWords.length; i++) {
await seedInputs.nth(i).fill(mnemonicWords[i])
}
await page.getByRole('button', { name: 'Import', exact: true }).click()
await page.getByPlaceholder('e.g. Trading, NFT Vault,').fill(name)
const inputs = await page.getByPlaceholder('At least 8 characters in length')
for (let i = 0; i < (await inputs.count()); i++) {
await inputs.nth(i).fill(process.env.TEST_WALLET_PASSWORD)
}
await page.getByRole('button', { name: /next/i }).click()
await page.getByRole('button', { name: /save/i }).click()
await page.close()
}

View file

@ -0,0 +1,17 @@
import { Page } from '@playwright/test'
export const setSeedPhrase = async ({
page,
seedPhrase,
}: {
page: Page
seedPhrase?: string
}) => {
if (!seedPhrase) {
await page.getByRole('button', { name: /continue with/i }).click()
} else {
await page.getByRole('button', { name: /enter my own/i }).click()
await page.locator('input').fill(seedPhrase)
await page.getByRole('button', { name: 'Continue', exact: true }).click()
}
}

View file

@ -0,0 +1,16 @@
import { BrowserContext, Page } from '@playwright/test'
export const signTx = async ({
context,
page,
}: {
context: BrowserContext
page: Page
}) => {
// Sign tx
const signPage = await context.waitForEvent('page')
await signPage.getByRole('button', { name: /approve/i }).click()
await signPage.waitForEvent('close')
await page.getByText(/successfully/i).waitFor({ state: 'visible' })
}

View file

@ -0,0 +1,19 @@
import { BrowserContext } from '@playwright/test'
export const swapWallet = async ({
context,
extensionUrl,
name,
}: {
context: BrowserContext
extensionUrl: string
name: string
}) => {
const page = await context.newPage()
await page.goto(`${extensionUrl}/popup.html`)
await page.locator('div[cursor="pointer"] > svg').nth(1).click()
await page.getByText(name).click()
await page.close()
}

View file

@ -0,0 +1,31 @@
import test from './fixtures'
import { connectWallet } from './helpers/connectWalet'
import { setSeedPhrase } from './helpers/setSeedPhrase'
test.beforeEach(async ({ context, page }) => {
await connectWallet({ context, page })
})
test.describe('Seed Phrase', () => {
test('can use autogenerated seed phrase', async ({ page }) => {
await setSeedPhrase({ page })
await test
.expect(
await page.evaluate(() =>
window.localStorage.getItem('ephemeral-mnemonic'),
),
)
.toBeDefined()
})
test('can enter and use a custom seed phrase', async ({ page }) => {
await setSeedPhrase({ page, seedPhrase: process.env.TEST_WALLET_MNEMONIC })
await test
.expect(
await page.evaluate(() =>
window.localStorage.getItem('ephemeral-mnemonic'),
),
)
.toEqual(process.env.TEST_WALLET_MNEMONIC)
})
})

View file

@ -1,15 +1,131 @@
import test from './fixtures'
import { importWallet } from './helpers/importWallet'
import { getBalance } from './helpers/getBalance'
import { swapWallet } from './helpers/swapWallet'
import { signTx } from './helpers/signTx'
import { connectWallet } from './helpers/connectWalet'
import { setSeedPhrase } from './helpers/setSeedPhrase'
test.beforeEach(async ({ page }) => {
await page.goto('/')
test.describe.configure({ mode: 'serial' })
test.beforeEach(async ({ context, page }) => {
await connectWallet({ context, page })
await setSeedPhrase({ page, seedPhrase: process.env.TEST_WALLET_MNEMONIC })
})
let mainBalance: number
test.describe('Transfers', () => {
test('app should render correctly', async ({ page }) => {
await test.expect(page.getByText('Balance:')).toBeVisible()
test('can deposit a sum successfully', async ({ context, page }) => {
// Initialize the balance
mainBalance = Number(
(await getBalance({ context, page }))!.replace('$', ''),
)
await page.getByRole('button', { name: /deposit/i }).click()
await page.keyboard.type('20')
await page
.getByRole('button', { name: /deposit/i })
.nth(1)
.click()
await signTx({ context, page })
await page
.getByRole('button', { name: /cancel/i, includeHidden: false })
.click()
// Check new balance
await page.waitForTimeout(4000)
mainBalance += 20
await test
.expect(await getBalance({ context, page }))
.toEqual(`$${mainBalance}`)
})
test('balance should be 0 at first', async ({ page }) => {
await test.expect(page.getByText('$0')).toBeVisible()
test('can transfer to another wallet successfully', async ({
context,
extensionUrl,
page,
}) => {
// Import a secondary wallet to transfer to
await importWallet({
extensionUrl,
mnemonic: process.env.TEST_SECONDARY_WALLET_MNEMONIC,
page: await context.newPage(),
name: 'secondary',
})
// Initialize the secondary account balance after importing
const secondaryBalance = Number(
(await getBalance({ context, page }))!.replace('$', ''),
)
// Swap back to main wallet
await swapWallet({ context, extensionUrl, name: 'main' })
// Transfer to the secondary wallet
await page.getByRole('button', { name: /transfer/i }).click()
await page.keyboard.type(process.env.TEST_SECONDARY_WALLET_ADDRESS)
await page.getByPlaceholder('0.00').fill('10')
await page
.getByRole('button', { name: /transfer/i })
.nth(1)
.click()
await signTx({ context, page })
await page
.getByRole('button', { name: /cancel/i, includeHidden: false })
.click()
// Check new balance
await page.waitForTimeout(4000)
mainBalance -= 10
await test
.expect(await getBalance({ context, page }))
.toEqual(`$${mainBalance}`)
// Swap to secondary to check if the transfer was received
await swapWallet({ context, extensionUrl, name: 'secondary' })
await test
.expect(await getBalance({ context, page }))
.toEqual(`$${secondaryBalance + 10}`)
// Set balance to 0 again for cleaning purposes
await page.getByRole('button', { name: /withdraw/i }).click()
await page
.getByRole('button', { name: /withdraw/i })
.nth(1)
.click()
await signTx({ context, page })
await page
.getByRole('button', { name: /cancel/i, includeHidden: false })
.click()
// Back to main wallet
await swapWallet({ context, extensionUrl, name: 'main' })
})
test('can withdraw deposited sum successfully', async ({ context, page }) => {
await page.getByRole('button', { name: /withdraw/i }).click()
await page
.getByRole('button', { name: /withdraw/i })
.nth(1)
.click()
await signTx({ context, page })
await page
.getByRole('button', { name: /cancel/i, includeHidden: false })
.click()
// Check new balance
await page.waitForTimeout(4000)
await test.expect(await getBalance({ context, page })).toEqual('$0')
})
})

View file

@ -13,9 +13,9 @@ async function globalSetup() {
const resp = await fetch(downloadUrl)
if (resp.ok && resp.body) {
Readable.fromWeb(resp.body as any).pipe(
unzipper.Extract({ path: folderPath }),
)
await Readable.fromWeb(resp.body as any)
.pipe(unzipper.Extract({ path: folderPath }))
.promise()
}
}
}