Skip to content

Commit

Permalink
test(e2e): add examples and docs for end-to-end testing (#331)
Browse files Browse the repository at this point in the history
  • Loading branch information
No-Cash-7970 authored Jan 10, 2025
1 parent 97fbae0 commit b4d34b8
Show file tree
Hide file tree
Showing 25 changed files with 814 additions and 25 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ dist
*.code-workspace
.nvmrc
coverage

# Playwright
**/test-results/
**/playwright-report/
**/blob-report/
**/playwright/.cache/
2 changes: 1 addition & 1 deletion docs/fundamentals/supported-wallets.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ KMD Documentation

## Mnemonic

The Mnemonic wallet provider is a specialized tool designed for testing purposes, particularly for integration tests. It should only be used in a test environment for running automated tests that require wallet interactions. (Documentation coming soon)
The Mnemonic wallet provider is a specialized tool designed for testing purposes, particularly for end-to-end and integration tests. It should only be used in a test environment for running automated tests that require wallet interactions. Refer to the [End-to-End Testing guide](../guides/end-to-end-testing.md) for more information about how to use the Mnemonic wallet provider for automated end-to-end testing.

{% hint style="danger" %}
**Warning:** The Mnemonic wallet provider is strictly for testing and development purposes. It will not function if the active network is set to MainNet. Any accounts used with the Mnemonic wallet should be considered insecure and should never hold MainNet ALGO or ASAs with any real value.
Expand Down
103 changes: 103 additions & 0 deletions docs/guides/end-to-end-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
description: Guide for end-to-end testing
---

# 🎯 End-to-End Testing

End-to-end (E2E) testing consists of creating and running automated tests that simulate the user going through various usage scenarios of the software from start (one end) to finish (the other end).[^1] E2E testing is also known as "system testing" because the tests are intended to test the system (software) as a whole.[^2] It does not replace unit testing, integration testing or manual testing. Instead, E2E testing should complement other types of testing.

In web development, an E2E test framework controls a web browser to test the web application. [Selenium WebDriver](https://www.selenium.dev/documentation/webdriver/), [Playwright](https://playwright.dev/) and [Cypress](https://www.cypress.io/) are common E2E test frameworks for web development. The best way of using an E2E test framework depends on the software requirements, code structure, and what other development tools are used.

## Wallet for E2E Testing

Most wallet applications, such as [Defly](../fundamentals/supported-wallets.md#defly) or [Pera](../fundamentals/supported-wallets.md#pera), do not allow for automated testing of decentralized apps (dApps) because they designed to require interaction from the human user. Requiring interaction from the human user is a critical part of the security of those wallet applications provide for users. However, this security is not needed for testing.

Fortunately, the [Mnemonic wallet provider](../fundamentals/supported-wallets.md#mnemonic) solves this problem, but at a heavy cost to the security of the accounts used. The Memonic wallet provider allows for an account's mnemonic ("seed phrase") to be entered directly. To automate the interaction with the Mnemonic wallet, a mnemonic **used only for testing** is often placed within the test _in plain text_.

### Setting Up Mnemonic Wallet

{% hint style="danger" %}
**Warning:** The Mnemonic wallet provider is strictly for testing and development purposes. It will not function if the active network is set to MainNet. Any accounts used with the Mnemonic wallet should be considered insecure and should never hold MainNet ALGO or ASAs with any real value.
{% endhint %}

To enable the Mnemonic wallet provider, add it to the list of wallets in the use-wallet [configuration](../fundamentals/get-started/configuration.md). The configuration should look something like the following code:

```typescript
import { NetworkId, WalletId, WalletManager } from '@txnlab/use-wallet'

const walletManager = new WalletManager({
wallets: [
WalletId.DEFLY,
WalletId.PERA,
{
id: WalletId.LUTE,
options: { siteName: '<YOUR_SITE_NAME>' }
},
WalletId.MNEMONIC, // <-- Add this
],
network: NetworkId.TESTNET
})
```

#### Persisting to Storage

By default for security reasons, the Mnemonic wallet provider does not save the mnemonic into local storage after it is entered and accepted. As a result, the wallet session is lost when reloading or exiting the page. The user needs to reconnect to the wallet by entering the mnemonic every time the page loads or reloads. This behavior is unlike most of the other wallet providers where the wallet session is immediately loaded and resumed when the loading the page.

The default behavior can be changed, but at an additional cost of security of the mnemonic and the account it is for. If you need the behavior of the Mnemonic wallet provider to be similar to the behavior of most of the other wallet providers in your tests, then enable persisting the mnemonic to storage. This way, **the mnemonic is stored into local storage indefinitely** and the saved wallet session can be loaded and resumed. The user enters the mnemonic once and only needs to enter it again after explicitly disconnecting from the wallet.

{% hint style="danger" %}
**Warning:** The mnemonic is stored into the local storage **in plain text**. Any mnemonic entered with persisting to storage enabled should be considered as compromised. Persisting the mnemonic to storage is strictly for testing and development purposes.
{% endhint %}

To enable persisting the mnemonic to storage, set the `persistToStorage` option for the Mnemonic wallet provider in the use-wallet [configuration](../fundamentals/get-started/configuration.md):

```typescript
import { NetworkId, WalletId, WalletManager } from '@txnlab/use-wallet'

const walletManager = new WalletManager({
wallets: [
WalletId.DEFLY,
WalletId.PERA,
{
id: WalletId.LUTE,
options: { siteName: '<YOUR_SITE_NAME>' }
},
{
id: WalletId.MNEMONIC,
options: { persistToStorage: true } // <-- Set this
},
],
network: NetworkId.TESTNET
})
```

## Testing with Playwright

[Playwright](https://playwright.dev/) can be used to test a web app built with any library or framework, such as React, Vue, Solid.js, or vanilla Javascript (no library or framework).

### Setting Up Playwright

To install Playwright, follow the instructions in Playwright's documentation: <https://playwright.dev/docs/intro#installing-playwright>

After installing Playwright, you can tweak its configuration for your project. There is an example `playwright.config.ts` file for each use-wallet example (in the [`examples/`](https://github.com/TxnLab/use-wallet/tree/main/examples) folder). For more information about how to configure Playwright, refer to its documentation: <https://playwright.dev/docs/test-configuration>

### Writing and Running Playwright Tests

Writing and running Playwright tests is the same for any web app, with or without use-wallet. Learn how to write and run tests in Playwright's documentation: <https://playwright.dev/docs/intro>.

There is an example Playwright E2E test in the [`examples/e2e-tests/` folder](https://github.com/TxnLab/use-wallet/tree/main/examples/e2e-tests). This single test can be run for any of the examples. To run the E2E test for an example, go to the chosen example folder (`vanilla-ts`, `react-ts`, etc.) and run `pnpm test`. For example, to run the E2E test for the vanilla TypeScript example, do the following:

```bash
cd examples/vanilla-ts
pnpm test:e2e
```

### Best Practices for Testing with Playwright

- For more consistent and predictable tests, mock the responses of API requests. Mocking also prevents overwhelming the API provider (like [Nodely](https://nodely.io/)) with test requests. An example of mocking responses to Algorand node (Algod) API requests is in the [`examples/e2e-tests/` folder](https://github.com/TxnLab/use-wallet/tree/main/examples/e2e-tests).
- More best practices: <https://playwright.dev/docs/best-practices>

## References

[^1]: <https://www.browserstack.com/guide/end-to-end-testing>
[^2]: <https://en.wikipedia.org/wiki/System_testing>
132 changes: 132 additions & 0 deletions examples/e2e-tests/FakeAlgodResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/** @file Algod node responses used to fake responses to requests */

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

/* NOTE:
* It is best for all test responses be exact data of actual responses from an Algod node. Doing so
* makes the fake responses as close as possible to what would most likely happen in the real world.
*/

// GET /v2/transactions/params on testnet
export const suggParams = JSON.stringify({
'consensus-version':
'https://github.com/algorandfoundation/specs/tree/925a46433742afb0b51bb939354bd907fa88bf95',
fee: 0,
'genesis-hash': 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=',
'genesis-id': 'testnet-v1.0',
'last-round': 44440857,
'min-fee': 1000
})

// POST /v2/transactions on testnet
export const sendTxn = JSON.stringify({
txId: 'NC63ESPZOQI6P6DSVZWG5K2FJFFKI3VAZITE5KRW5SV5GXQDIXMA'
})

// GET /v2/status on testnet
export const nodeStatus = JSON.stringify({
catchpoint: '',
'catchpoint-acquired-blocks': 0,
'catchpoint-processed-accounts': 0,
'catchpoint-processed-kvs': 0,
'catchpoint-total-accounts': 0,
'catchpoint-total-blocks': 0,
'catchpoint-total-kvs': 0,
'catchpoint-verified-accounts': 0,
'catchpoint-verified-kvs': 0,
'catchup-time': 0,
'last-catchpoint': '',
'last-round': 44440860,
'last-version':
'https://github.com/algorandfoundation/specs/tree/925a46433742afb0b51bb939354bd907fa88bf95',
'next-version':
'https://github.com/algorandfoundation/specs/tree/925a46433742afb0b51bb939354bd907fa88bf95',
'next-version-round': 44440861,
'next-version-supported': true,
'stopped-at-unsupported-round': false,
'time-since-last-round': 1753631441
})

// GET /v2/transactions/pending/NC63ESPZOQI6P6DSVZWG5K2FJFFKI3VAZITE5KRW5SV5GXQDIXMA?format=msgpack
// content-type: application/msgpack
// on testnet
export const pendingTxn1 = Buffer.from(
'gqpwb29sLWVycm9yoKN0eG6Co3NpZ8RAiG8Nhiruhncf2es5ozYnVfiFY4EAvLiGODPZf2n0eI4X1VtBZScF+3WQwn2RsIkdMyHbG0FNb5sQ93R03WTgAqN0eG6Io2ZlZc0D6KJmds4Cph0Zo2dlbqx0ZXN0bmV0LXYxLjCiZ2jEIEhjtRiks8hOyBDyLU8QgcsPcfBZp6wg3sYvf3DlCToiomx2zgKmIQGjcmN2xCDZdlfb2YQwPyRi+VSoHaataICjLqI7Z8kkOGIA5HFvlqNzbmTEINl2V9vZhDA/JGL5VKgdpq1ogKMuojtnySQ4YgDkcW+WpHR5cGWjcGF5',
'base64'
)

// GET /v2/transactions/pending/NC63ESPZOQI6P6DSVZWG5K2FJFFKI3VAZITE5KRW5SV5GXQDIXMA?format=msgpack
// content-type: application/msgpack
// on testnet
export const pendingTxn2 = Buffer.from(
'g69jb25maXJtZWQtcm91bmTOAqYdHqpwb29sLWVycm9yoKN0eG6Co3NpZ8RAiG8Nhiruhncf2es5ozYnVfiFY4EAvLiGODPZf2n0eI4X1VtBZScF+3WQwn2RsIkdMyHbG0FNb5sQ93R03WTgAqN0eG6Io2ZlZc0D6KJmds4Cph0Zo2dlbqx0ZXN0bmV0LXYxLjCiZ2jEIEhjtRiks8hOyBDyLU8QgcsPcfBZp6wg3sYvf3DlCToiomx2zgKmIQGjcmN2xCDZdlfb2YQwPyRi+VSoHaataICjLqI7Z8kkOGIA5HFvlqNzbmTEINl2V9vZhDA/JGL5VKgdpq1ogKMuojtnySQ4YgDkcW+WpHR5cGWjcGF5',
'base64'
)

// GET /v2/status/wait-for-block-after/44440861
export const waitForBlock = JSON.stringify({
catchpoint: '',
'catchpoint-acquired-blocks': 0,
'catchpoint-processed-accounts': 0,
'catchpoint-processed-kvs': 0,
'catchpoint-total-accounts': 0,
'catchpoint-total-blocks': 0,
'catchpoint-total-kvs': 0,
'catchpoint-verified-accounts': 0,
'catchpoint-verified-kvs': 0,
'catchup-time': 0,
'last-catchpoint': '',
'last-round': 44440862,
'last-version':
'https://github.com/algorandfoundation/specs/tree/925a46433742afb0b51bb939354bd907fa88bf95',
'next-version':
'https://github.com/algorandfoundation/specs/tree/925a46433742afb0b51bb939354bd907fa88bf95',
'next-version-round': 44440863,
'next-version-supported': true,
'stopped-at-unsupported-round': false,
'time-since-last-round': 567756
})

/** Fake the responses to a series of Algod requests for sending a simple transaction. The faked
* responses are actual responses from an Algod node when sending a real transaction on TestNet.
*
* Faking the responses of an Algod node makes the tests more consistent, puts less strain on an
* actual Algod node, and removes the requirement of a real Algod node being available before
* running the tests.
*
* NOTE: For the faked responses to work, this function must be run after the page is loaded (e.g.
* after `page.goto("/")`).
*/
export async function fakeTxnResponses(page: Page) {
await page.route('*/**/v2/transactions/params', async (route) => {
await route.fulfill({ body: suggParams, contentType: 'application/json' })
})

await page.route('*/**/v2/transactions', async (route, request) => {
if (request.method() === 'OPTIONS') {
await route.fulfill()
} else {
await route.fulfill({ body: sendTxn, contentType: 'application/json' })
}
})

await page.route('*/**/v2/status', async (route) => {
await route.fulfill({ body: nodeStatus, contentType: 'application/json' })
})

let pendingTxnCount = 0
await page.route('*/**/v2/transactions/pending/*', async (route) => {
if (pendingTxnCount === 0) {
// First time
await route.fulfill({ body: pendingTxn1, contentType: 'application/msgpack' })
pendingTxnCount++
} else {
// Second time
await route.fulfill({ body: pendingTxn2, contentType: 'application/msgpack' })
}
})

await page.route('*/**/v2/status/wait-for-block-after/*', async (route) => {
await route.fulfill({ body: waitForBlock, contentType: 'application/json' })
})
}
42 changes: 42 additions & 0 deletions examples/e2e-tests/example.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test'
import { fakeTxnResponses } from './FakeAlgodResponses'

test('it works', async ({ page }) => {
// Load and set up the page
await page.goto('/')
await fakeTxnResponses(page)
// Whenever a prompt appears, enter the mnemonic
page.on('dialog', (dialog) =>
dialog.accept(
// !! WARN !!
// THIS ACCOUNT AND ITS MNEMONIC ARE COMPROMISED.
// They are to be used for testing only.
// !! WARN !!
'sugar bronze century excuse animal jacket what rail biology symbol want craft annual soul increase question army win execute slim girl chief exhaust abstract wink'
)
)

// Check mnemonic wallet is activated
await expect(page.getByRole('heading', { name: 'Mnemonic' })).toBeVisible()

// Click the "Connect" button for the Mnemonic wallet
await page
.locator('.wallet-group', {
has: page.locator('h4', { hasText: 'Mnemonic' })
})
.getByRole('button', { name: 'Connect', exact: true })
.click()

// Check wallet is connected
await expect(page.getByRole('heading', { name: 'Mnemonic [active]' })).toBeVisible()
await expect(page.getByRole('combobox')).toHaveValue(
'3F3FPW6ZQQYD6JDC7FKKQHNGVVUIBIZOUI5WPSJEHBRABZDRN6LOTBMFEY'
)

// Click button to send a transaction
await page.getByRole('button', { name: 'Send Transaction' }).click()

// There is no visual feedback of the outcome of sending the transaction. Only a message is
// printed in the console. So, we will wait a little bit for transaction to complete
await page.waitForTimeout(500)
})
3 changes: 2 additions & 1 deletion examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test:e2e": "playwright test"
},
"dependencies": {
"@blockshake/defly-connect": "^1.1.6",
Expand Down
75 changes: 75 additions & 0 deletions examples/nextjs/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { defineConfig, devices } from '@playwright/test'

// Use process.env.PORT by default and fallback to port 3000
const PORT = process.env.PORT || 3000

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: '../e2e-tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'list',
/* 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('/')`. */
baseURL: `http://localhost:${PORT}`,

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
webServer: {
command: 'pnpm build && pnpm start',
url: `http://localhost:${PORT}`,
reuseExistingServer: !process.env.CI,
stdout: 'pipe'
}
})
3 changes: 2 additions & 1 deletion examples/nextjs/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const walletManager = new WalletManager({
{
id: WalletId.MAGIC,
options: { apiKey: 'pk_live_D17FD8D89621B5F3' }
}
},
WalletId.MNEMONIC
],
network: NetworkId.TESTNET
})
Expand Down
Loading

0 comments on commit b4d34b8

Please sign in to comment.