-
-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(e2e): add examples and docs for end-to-end testing (#331)
- Loading branch information
1 parent
97fbae0
commit b4d34b8
Showing
25 changed files
with
814 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.